LINQ to Excel を作ってみる その3
色々調べていてやっとこさ仕組みがわかってきたので、少し進める。
まず、XlsWorksheetsクラスをIQueryProviderインターフェースを実装するように変更する。
XlsWorksheets.cs
using System; using System.Linq; using System.Linq.Expressions; using System.Collections; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Diagnostics; using Excel.Interop; namespace Excel.Linq { /// <summary> /// Excelワークシートのコレクションに対する操作を提供するクラス /// </summary> public class XlsWorksheets : IQueryable<XlsWorksheet>, IQueryProvider, IDisposable { private Sheets worksheets; private Expression expression; /// <summary> /// 生のWorksheetオブジェクトを設定するコンストラクタ /// </summary> /// <param name="worksheets">Worksheetオブジェクト</param> protected internal XlsWorksheets(Sheets worksheets) { this.worksheets = worksheets; } /// <summary> /// 生のWorksheetオブジェクトと列挙結果をフィルタリングする式を設定するコンストラクタ /// </summary> /// <param name="worksheets">Worksheetsオブジェクト</param> /// <param name="expression">式</param> private XlsWorksheets(Sheets worksheets, Expression expression) : this(worksheets) { this.expression = expression; } ~XlsWorksheets() { Dispose(); } /// <summary> /// 指定した式を条件として、オブジェクトの列挙を行います。 /// </summary> /// <param name="expression">式</param> /// <returns>コレクション</returns> private IEnumerable<XlsWorksheet> ExecuteExpression(Expression expression) { Predicate<Worksheet> predicate = ParseExpression(expression); foreach(Worksheet worksheet in worksheets) { if(predicate(worksheet)) yield return new XlsWorksheet(worksheet); } } /// <summary> /// 指定した式を解析して、適切なデリゲートに変換します。 /// </summary> /// <param name="expression">式</param> /// <returns>デリゲート</returns> private Predicate<Worksheet> ParseExpression(Expression expression) { LambdaExpression lexpr = Expression.Lambda( Expression.Constant(true), Expression.Parameter(typeof(Worksheet), "s") ); return (Predicate<Worksheet>)lexpr.Compile(); } /// <summary> /// 式無しでアイテムの列挙のみを行います。 /// </summary> /// <returns>コレクション</returns> private IEnumerable<XlsWorksheet> ForEachWithoutExpression() { foreach(Worksheet sheet in worksheets) yield return new XlsWorksheet(sheet); } // IEnumerable<XlsWorksheet> メンバ public IEnumerator<XlsWorksheet> GetEnumerator() { return Provider.Execute<IEnumerator<XlsWorksheet>>(Expression); } IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); } // IQueryable<XlsWorksheet> メンバ public Type ElementType { get { return typeof(XlsWorksheet); } } public Expression Expression { get { return expression; } } public IQueryProvider Provider { get { return this; } } // IQueryProvider メンバ public IQueryable<TElement> CreateQuery<TElement>(Expression expression) { return (IQueryable<TElement>)CreateQuery(expression); } public IQueryable CreateQuery(Expression expression) { return new XlsWorksheets(this.worksheets, expression); } public TResult Execute<TResult>(Expression expression) { return (TResult)Execute(expression); } public object Execute(Expression expression) { return (expression != null ? ExecuteExpression(expression) : ForEachWithoutExpression() ).GetEnumerator(); } protected virtual void Dispose(bool disposing) { if(!disposing) return; if(worksheets != null) { Marshal.ReleaseComObject(worksheets); worksheets = null; } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } } }
IQueryable
public Type ElementType { get { return typeof(XlsWorksheet); } } public Expression Expression { get { return expression; } } public IQueryProvider Provider { get { return this; } }
ElementTypeプロパティではXlsWorksheetの型を、Expressionプロパティではフィールドで保持したexpression変数を返す。Providerプロパティでは自分自身を返すようにする。
IQueryProviderインターフェースで実装するメソッドは、
public IQueryable<TElement> CreateQuery<TElement>(Expression expression) { return (IQueryable<TElement>)CreateQuery(expression); } public IQueryable CreateQuery(Expression expression) { return new XlsWorksheets(this.worksheets, expression); } public TResult Execute<TResult>(Expression expression) { return (TResult)Execute(expression); } public object Execute(Expression expression) { return (expression != null ? ExecuteExpression(expression) : ForEachWithoutExpression() ).GetEnumerator(); }
IQueryableを返すCreateQueryメソッドのジェネリック版と非ジェネリック版、objectを返すExecuteメソッドのジェネリック版と非ジェネリック版をそれぞれ実装する。
メソッド名から推測するに、IQueryProviderはクエリの構築や実行を行うクラスが実装するインターフェースだろう。
ここでは、CreateQueryメソッドで引数として渡されてきたExpressionをコンストラクタの引数としてXlsWorksheetsを新しくインスタンス化して返す。
ここで呼び出したコンストラクタでは以下のようにexpressionをフィールドに設定している。
private XlsWorksheets(Sheets worksheets, Expression expression) : this(worksheets) { this.expression = expression; }
Executeメソッドでは、expressionの有無で処理を切り分けている。戻り値に対してGetEnumeratorメソッドを呼び出しているのは、このメソッドの戻り値がobjectであるため、そのままでは列挙が開始されないからである。
ForEachWithoutExpressionメソッドでは、
private IEnumerable<XlsWorksheet> ForEachWithoutExpression() { foreach(Worksheet sheet in worksheets) yield return new XlsWorksheet(sheet); }
今まで通りの方法で、全シートを列挙する。
ExecuteExpressionメソッドが鍵になるわけだが、その前にこのクラスを機能させるためにwhere演算子をオーバーライドする必要がある。
オーバーライドすると言ってもoverride*1とか使ってやるわけではなく、以下のようなシグネチャでstaticなWhereメソッドを作成する。
XlsExtension.cs
using System; using System.Linq; using System.Linq.Expressions; using System.Collections.Generic; using System.Diagnostics; namespace Excel.Linq { /// <summary> /// Excel.Linq の各クラスに対して拡張メソッドを提供するクラス /// </summary> public static class XlsExtension { public static IQueryable<XlsWorksheet> Where( this IQueryable<XlsWorksheet> collection, Expression<Predicate<XlsWorksheet>> expression) { return collection.Provider.CreateQuery<XlsWorksheet>(expression); } } }
ここではIQueryable経由でCreateQueryメソッドを呼び出すだけ。
二つ目の引数をExpressionにする事で、where演算子に渡されたラムダ式を取得する事ができる。これで後続の処理でこのラムダ式を解析して、独自のフィルタリング処理を実装するわけだ。
IQueryableのProviderプロパティ経由でIQueryProviderの実装クラスのインスタンスを取得するわけだから、実際どのクラスがIQueryProviderを実装しているかを知る必要が無いというのもよくできている。まさにそのためにIQueryableにProviderプロパティが用意されているんだろう。
また、このメソッドのシグネチャは以下のように書く事もできる。
public static IEnumerable<XlsWorksheet> Where( this IEnumerable<XlsWorksheet> collection, Predicate<XlsWorksheet> predicate) { }
この場合はExpressionを取得する事ができない。
さてExecuteExpressionメソッドの実装だが、Expressionを解析してPredicate
private IEnumerable<XlsWorksheet> ExecuteExpression(Expression expression) { Predicate<Worksheet> predicate = ParseExpression(expression); foreach(Worksheet worksheet in worksheets) { if(predicate(worksheet)) yield return new XlsWorksheet(worksheet); } } private Predicate<Worksheet> ParseExpression(Expression expression) { // ↓こういうラムダ式 // s => true LambdaExpression lexpr = Expression.Lambda( Expression.Constant(true), Expression.Parameter(typeof(Worksheet), "s") ); return (Predicate<Worksheet>)lexpr.Compile(); }
現段階のParseExpressionメソッドではtrueを返すだけのラムダ式を生成して、それをCompileメソッドでPredicate
ここで渡ってくるexpressionはPredicate
ここの変換する処理をいったいどうやって実装すればいいのかと悩んでいたが、よく考えてみればExpressionTreeを解析していって、XlsWorksheetがパラメータとして渡されているところをWorksheetに変えて、それに対するメンバ呼び出しも対応するもの*2に変えて、ExpressionTreeを再構築してやればいいだけだと気がついた。
次はその部分を実装する。しかしLINQを実現するための仕組みはすごいな!
目次
LINQ to Excel を作ってみる その4
前回途中で終わったParseExpressionメソッドを実装した。
その前にそもそも何がしたいかというと、XlsWorksheetsクラスではワークシートを列挙する時、以下のようにWorksheetオブジェクトをラップしたXlsWorksheetクラスをインスタンス化して返していた。
foreach(Worksheet sheet in worksheets) yield return new XlsWorksheet(sheet);
これに対してLINQのwhere演算子で以下のようにフィルタリングを掛けると、当たり前だがこの条件にマッチしないシートまでXlsWorksheetクラスでラップされて列挙される。
var sheets = from s in book.Worksheets where s.Name == "100" select s;
これを防ぐため、where演算子で指定された条件にマッチしたシートのみをXlsWorksheetクラスでラップして列挙するようにしたい。そのためにwhere演算子をオーバーライドし、シートの列挙操作の前にフィルタリング処理を追加したわけだ。
で、そのフィルタリング処理にはPredicate
それをどのように作るかといえば、where演算子で渡されてくるExpressionはExpression
具体的にどのように再構築するかといえば、
- ExpressionTreeを上から順に辿っていく。
- インスタンス化したExpressionでExpressionTreeを構築する。
ということで、ParseExpressionメソッドの実装から。
XlsWorksheets.cs
private Predicate<Worksheet> ParseExpression(Expression expression) { Expression lexpr = RebuildExpression(expression); return (Predicate<Worksheet>)((LambdaExpression)lexpr).Compile(); } private Expression RebuildExpression(Expression expression) { return new ExpressionRebuilder("s").Rebuild(expression); }
RebuildExpressionメソッドでExpressionTreeを再構築している。その中ではExpressionRebuilderクラスをインスタンス化して、Rebuildメソッドを呼び出している。
ExpressionRebuilderの実装は以下。XlsWorksheetsのインナークラスとして実装した。
XlsWorksheets.cs
private class ExpressionRebuilder { /// <summary> /// XlsWorksheet型のParameterExpressionと置換するParameter /// </summary> private ParameterExpression paramExpr; public ExpressionRebuilder(string name) { paramExpr = Expression.Parameter(typeof(Worksheet), name); } public Expression Rebuild(Expression expr) { BinaryExpression binExpr; switch(expr.NodeType) { case ExpressionType.Lambda: return Expression.Lambda<Predicate<Worksheet>>( Rebuild(((LambdaExpression)expr).Body), ((LambdaExpression)expr).Parameters.ToList().ConvertAll<ParameterExpression>( p => (ParameterExpression)Rebuild(p) ) ); case ExpressionType.Equal: case ExpressionType.NotEqual: case ExpressionType.GreaterThan: case ExpressionType.GreaterThanOrEqual: case ExpressionType.LessThan: case ExpressionType.LessThanOrEqual: binExpr = (BinaryExpression)expr; return (Expression)typeof(Expression).InvokeMember(expr.NodeType.ToString(), BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod, null, null, new object[] { Rebuild(binExpr.Left), Rebuild(binExpr.Right), binExpr.IsLiftedToNull, binExpr.Method } ); case ExpressionType.And: case ExpressionType.AndAlso: case ExpressionType.Or: case ExpressionType.OrElse: binExpr = (BinaryExpression)expr; return (Expression)typeof(Expression).InvokeMember(expr.NodeType.ToString(), BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod, null, null, new object[] { Rebuild(binExpr.Left), Rebuild(binExpr.Right), binExpr.Method } ); case ExpressionType.Parameter: return ((ParameterExpression)expr).Type == typeof(XlsWorksheet) ? paramExpr : expr; case ExpressionType.MemberAccess: MemberExpression mexpr = (MemberExpression)expr; // XlsWorksheet // MemberInfo member = mexpr.Member.DeclaringType != typeof(XlsWorksheet) ? mexpr.Member : ((Func<MemberInfo, MemberInfo>)((m) => { if(m.Name == "Name") return typeof(_Worksheet).GetProperty("Name"); return typeof(_Worksheet).GetProperty(m.Name); }))(mexpr.Member); return Expression.Property( Rebuild(mexpr.Expression), (PropertyInfo)member ); } return expr; } }
DLINQにしてもXLINQにしてもこんな感じでExpressionTreeを解析しSQLやらXPathを構築した後、その結果のみを列挙していく。こうすることで、使う人間からはあたかもオブジェクトに対して条件を与えてフィルタリングやらソートをしているように見せて、その内部ではデータの種類に応じて最適な方法で処理を行う。
そういった事をLINQは拡張メソッドやラムダ式を使って実現している。いや、LINQを実現するために拡張メソッドとラムダ式が作られたのか。
とりあえず終わり、これの全ソースはまたCodePlexででも公開します。