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ででも公開します。