「C# Advent Calendar 2014」の12日目の記事。
以前、「[C#][式木][LINQ] Hokuriku.NET C# 勉強会『C# 式木』(2014-10-26、金沢) のスライド公開」で、IQueryable な LINQ について解説した。
本記事では、その中の IQueryable なサンプルを補足する。
IQueryable な LINQ の中はどのようになっているのだろうか。
試しに少し実装してみることで、LINQ について理解を深めよう。
IEnumerable と IQueryable
「[C#][ラムダ式][LINQ][式木] 匿名メソッドとラムダ式の違い」で紹介したように、匿名メソッドは delegate としてしか使えないが、ラムダ式は delegate としても式木としても使うことができる。
※ [C#][ラムダ式][式木] Expression として扱えるラムダ式と扱えないラムダ式」で紹介したように、ラムダ式であれば必ず式木として使うことができるわけではない。
※ クエリ構文は、「式木として扱えるラムダ式」の糖衣構文。つまり、式木を扱うことになる。
参考: LINQ でのクエリ構文とメソッド構文 (C#) - MSDN
LINQ の中には、次の二つの種類のライブラリがある。
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 でインストールできる。