[C#][式木][LINQ] IQueryable な Twitter のタイムライン クラスと LINQ プロバイダー
「C# Advent Calendar 2014」の12日目の記事。
以前、「[C#][式木][LINQ] Hokuriku.NET C# 勉強会『C# 式木』(2014-10-26、金沢) のスライド公開」で、IQueryable
な LINQ について解説した。
- LINQ to Objects 復習
- IQueryable<T>
- 式木 (Expression Tree)
- 式木メタ プログラミング
- LINQ プロバイダー
本記事では、その中の
IQueryable なサンプルを補足する。
IQueryable な LINQ の中はどのようになっているのだろうか。
試しに少し実装してみることで、LINQ について理解を深めよう。
IEnumerable と IQueryable
「[C#][ラムダ式][LINQ][式木] 匿名メソッドとラムダ式の違い」で紹介したように、匿名メソッドは delegate としてしか使えないが、ラムダ式は delegate としても式木としても使うことができる。
※ [C#][ラムダ式][式木] Expression として扱えるラムダ式と扱えないラムダ式」で紹介したように、ラムダ式であれば必ず式木として使うことができるわけではない。
※ クエリ構文は、「式木として扱えるラムダ式」の糖衣構文。つまり、式木を扱うことになる。
参考: LINQ でのクエリ構文とメソッド構文 (C#) – MSDN
LINQ の中には、次の二つの種類のライブラリがある。
- delegate を引数にしたもの
- 例. IEnumerable<T>.Where(匿名メソッド)
- 式木を引数にしたもの
- 例. IQueryable<T>.Where(ラムダ式)
LINQ to Objects などは前者で処理され、LINQ to SQL、LINQ to Entities などは後者だ。
IQueryable なものを作ってみよう
今回は、IQueryable な Twitter のライムライン クラスを作ろうとしてみる。
先ずは IQueryable なクラス QueryableTweets。
※ IQueryable なだけでは OrderBy の対象となることができないので、ここでは IQueryable からの派生で OrderBy 可能な
IOrderedQueryable を用いることにする。
// QueryableTweets.cs using System.Linq; // IOrderedQueryable<string> な QueryableTweets // まだ IOrderedQueryable インタフェイスを実装してないのでコンパイル エラー public class QueryableTweets<TElement> : IOrderedQueryable<TElement> {}
ここから、Visual Studio でインタフェイスの実装を行うと次のようになる。
// QueryableTweets.cs using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; // IOrderedQueryable<string> な QueryableTweets // Visual Studio でインタフェイスを実装した直後 public class QueryableTweets<TElement> : IOrderedQueryable<TElement> { public Type ElementType { get { throw new NotImplementedException(); } } public Expression Expression { get { throw new NotImplementedException(); } } public IQueryProvider Provider { get { throw new NotImplementedException(); } } public IEnumerator<TElement> GetEnumerator() { throw new NotImplementedException(); } IEnumerator IEnumerable.GetEnumerator() { throw new NotImplementedException(); } }
IQueryable は IEumerable から派生している。そのため IEumerable のメンバーである GetEnumerator()
を実装する必要がある。
その他に、ElementType、Expression、Provider というプロパティを実装しなければならない。
実装を進めていこう。このクラスの実装はそれほど大変ではない。
Provider プロパティのために IQueryProvider インタフェイスを持つクラスを用意する必要があるが、ここでは、それを仮に TwitterQueryProvider クラスとしておこう。
TwitterQueryProvider クラスは後述する。
// QueryableTweets.cs using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; // IOrderedQueryable<string> な QueryableTweets public class QueryableTweets<TElement> : IOrderedQueryable<TElement> { public Type ElementType { get { return typeof(TElement); } } public Expression Expression { get; set; } public IQueryProvider Provider { get; set; } public QueryableTweets() { Provider = new TwitterQueryProvider(); // IQueryProvider インタフェイスを実装したクラス。後述。 Expression = Expression.Constant(this); } public IEnumerator<TElement> GetEnumerator() { return ((IEnumerable<TElement>)Provider.Execute(Expression)).GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } }
IQueryProvider なもの (LINQ プロバイダー) を作ろうとしてみよう
続いて、上記 QueryableTweets で使うための、IQueryProvider なものの実装だ。
これは、LINQ プロバイダーと呼ばれるもので、式木としてのクエリーを解釈する。
こちらの実装は大変だ。
クラス名を TwitterQueryProvider として、IQueryProvider を実装していこう。
// TwitterQueryProvider.cs using System.Linq; // LINQ プロバイダーの実験用 // まだ IQueryProvider インタフェイスを実装してないのでコンパイル エラー public class TwitterQueryProvider : IQueryProvider {}
ここから、Visual Studio でインタフェイスの実装を行うと次のようになる。
// TwitterQueryProvider.cs using System; using System.Linq; using System.Linq.Expressions; // LINQ プロバイダーの実験用 // Visual Studio でインタフェイスを実装した直後 public class TwitterQueryProvider : IQueryProvider { public IQueryable CreateQuery(Expression expression) { throw new NotImplementedException(); } public IQueryable<TElement> CreateQuery<TElement>(Expression expression) { throw new NotImplementedException(); } public object Execute(Expression expression) { throw new NotImplementedException(); } public TResult Execute<TResult>(Expression expression) { throw new NotImplementedException(); } }
少し実装を進めてみる。
// TwitterQueryProvider.cs using System; using System.Linq; using System.Linq.Expressions; // LINQ プロバイダーの実験用 public class TwitterQueryProvider : IQueryProvider { IQueryable IQueryProvider.CreateQuery(Expression expression) { return null; } public IQueryable<TElement> CreateQuery<TElement>(Expression expression) { return new QueryableTweets<TElement> { Provider = this, Expression = expression }; } public TResult Execute<TResult>(Expression expression) { return default(TResult); } public object Execute(Expression expression) { // ここで式木を解釈して、コレクションを作って返す return null; // とりあえずは仮に null を返すだけにしておく } }
この中で、ポイントとなるのは Execute メソッドだ。
この Execute メソッドには、式木が渡ってくる。この式木を解釈してやって、そこからコレクションとしての結果を返してやれば良い。
ここは後で実装することにして、とりあえずは null を返すだけにしておく。
ExpressionVisitor の派生クラスで Visitor パターンによる式木の解釈
LINQ プロバイダーの Execute メソッドでの式木を解釈だが、それには、ExpressionVisitor というクラスが使える。
ExpressionVisitor から派生することで、Visitor パターンによる解析が可能となる。
参考: ExpressionVisitor クラス – MSDN
LINQ のための式木をきちんと解釈するのは、かなり大変なことだ。
ここでは、ごく一部の構文にだけ注目して、そこのみに対応することにする。
取り敢えずの最低限のサンプル コード、Where(text => text.Contains(“C#”)) の形にのみ対応してみる。
尚、この中では、Twitter のタイムラインを取得する TwitterTimeline クラスを使っているが、 TwitterTimeline クラスは後述する。
// TwitterExpressionVisitor.cs using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; // Visitor パターンで式木を解析して検索用文字列を取り出し、それを使って Twitter のタイムラインを取得する public class TwitterExpressionVisitor : ExpressionVisitor { public IEnumerable<string> Statuses { get; private set; } // 取り敢えずの最低限のサンプル コード // Where(text => text.Contains("C#")) の形にのみ対応してみる protected override Expression VisitMethodCall(MethodCallExpression expression) { // もし Where メソッドを呼ぶ式だったら if (expression.Method.Name == "Where") { // Where メソッドの第二引数であるラムダ式を取り出す var lambdaExpression = (LambdaExpression)((UnaryExpression)(expression.Arguments[1])).Operand; // そのラムダ式の Body 部を取り出す var bodyExpression = lambdaExpression.Body as MethodCallExpression; // もし Contains メソッドを呼ぶ式で if (bodyExpression != null && bodyExpression.Method.Name == "Contains") { // その引数が定数式だったら var constantExpression = bodyExpression.Arguments[0] as ConstantExpression; if (constantExpression != null) { // その定数の値を検索文字列とし var searchText = constantExpression.Value as string; if (searchText != null) // TwitterTimeline クラス (後述) を使って、タイムラインからその検索文字列にあたる Status を取得しておく Statuses = new TwitterTimeline().Filter(searchText).Select(status => status.Text); } } } return base.VisitMethodCall(expression); } }
TwitterTimeline クラスによる Twitter タイムラインの取得
次に、Twitter のタイムラインを取得するためのダミー クラス TwitterTimeline を用意する。
実際に Twitter のタイムラインを取得するコードを用意すれば良いわけだが、今回は説明の簡略化のために CoreTweet というライブラリとダミー コードを用いることにする。
CoreTweet は、Visual Studio から NuGet でインストールできる。

ダミー コードは次の通り。
// TwitterTimeline.cs using CoreTweet; // Twitter のタイムライン取得用 using System.Collections; using System.Collections.Generic; using System.Linq; // Twitter のタイムライン取得用 // CoreTweet ( https://github.com/CoreTweet/CoreTweet/wiki/Home(%E6%97%A5%E6%9C%AC%E8%AA%9E) ) を利用 // NuGet でインストールできる // Twitterの開発者向けサイト "Twitter Developers" ( https://dev.twitter.com ) にアプリケーションの登録をし、 // Consumer Key、Consumer Secret、Access Token、Access Secret を取得するなどすれば、 // 実際に Twitter のタイムラインから取得することも可能 class TwitterTimeline : IEnumerable<Status> { public IEnumerable<Status> Filter(string searchText) { // ダミー実装 // 実際には、ここで searchText にマッチする Status のみを取ってくるのが良い return this.Where(status => status.Text.Contains(searchText)); } public IEnumerator<Status> GetEnumerator() { // "Twitter Developers" に登録し、キーやトークンを取得すれば、実際に Twitter のタイムラインを取れる //var tokens = CoreTweet.Tokens.Create("[Your Consumer Key]", "[Your Consumer Secret]", // "[Your Access Token]", "[Your Access Secret]"); //return tokens.Statuses.HomeTimeline().GetEnumerator(); // ダミー実装 // 実際には Twitter のタイムラインを取ってくる yield return new Status { Text = "C# で CoreTweet を使って Twitter のタイムラインを取得してみた" }; yield return new Status { Text = "式木いじりは茨の道" }; yield return new Status { Text = "C# で LINQ を使う" }; yield return new Status { Text = "Hokuriku,NET C# 式木" }; } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } }
LINQ プロバイダー TwitterQueryProvider への組み込み
では、TwitterQueryProvider に TwitterExpressionVisitor を組み込んでみよう。
Execute メソッドの中で、TwitterExpressionVisitor の Visit を呼ぶ。
すると、TwitterExpressionVisitor が式木を解釈し、結果を Statuses に入れるので、それを返せば OK だ。
// TwitterQueryProvider.cs using System; using System.Linq; using System.Linq.Expressions; // LINQ プロバイダーの実験用 public class TwitterQueryProvider : IQueryProvider { IQueryable IQueryProvider.CreateQuery(Expression expression) { return null; } public IQueryable<TElement> CreateQuery<TElement>(Expression expression) { return new QueryableTweets<TElement> { Provider = this, Expression = expression }; } public TResult Execute<TResult>(Expression expression) { return default(TResult); } public object Execute(Expression expression) { // ここで式木を解釈して、コレクションを作って返す var expressionVisitor = new TwitterExpressionVisitor(); expressionVisitor.Visit(expression); var statuses = expressionVisitor.Statuses; return statuses.AsQueryable<string>(); } }
テスト
使ってみよう。
コンソール アプリケーションの Main から使用してみる。
// Program.cs using System; using System.Linq; class Program { static void Main() { IQueryable<string> query1 = new QueryableTweets<string>(); IQueryable<string> query2 = query1.Where(text => text.Contains("C#")); // ここまでは、式木をつくっているだけ Console.WriteLine(query2.Expression); Console.WriteLine(); // 下の foreach 中で実際に値を item に取り出そうとすると、 // 1. TwitterQueryProvider の Execute にその式木が渡され、 // 2. TwitterExpressionVisitor でそれが解析される中で、 // 3. TwitterTimeline がタイムラインの取得を行う foreach (var item in query2) Console.WriteLine(item); } }
実行結果は、次の通りだ。
value(QueryableTweets`1[System.String]).Where(text => text.Contains("C#")) C# で CoreTweet を使って Twitter のタイムラインを取得してみた C# で LINQ を使う Hokuriku,NET C# 式木
始めに式木が表示され、その後で、クエリーの結果 "C#" が含まれる Status が三行表示された。
このサンプルコードの範囲ではうまく動いたようだ。
■ 今回のまとめ
今回は、Hokuriku.NET C# 勉強会『C# 式木』 で説明した内容を補足した。
IQueryable なものを書き LINQ に対応させるのはかなり大変だが、その意味するところだけでも理解していけば、LINQ について理解を深めることができるように思う。
ディスカッション
コメント一覧
まだ、コメントがありません