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を解析して作る必要がある。

それをどのように作るかといえば、where演算子で渡されてくるExpressionはExpression>なので、このExpressionTreeを解析しExpression>に再構築してやればいい。そこからCompileメソッドを呼び出せば、目的のデリゲートを作る事ができる。

具体的にどのように再構築するかといえば、

  1. ExpressionTreeを上から順に辿っていく。
    1. XlsWorksheetクラスをTypeプロパティに持つParameterExpressionの場合はTypeをWorksheetクラスに変えてParameterExpressionをインスタンス化する。
    2. XlsWorksheetクラスに対するメンバアクセスを行っているMemberExpressionの場合は、アクセス対象をWorksheetクラスに変え、メンバ呼び出しを対応するWorksheetクラスのメンバ呼び出しに変え、MemberExpressionをインスタンス化する。
    3. 上記以外の場合は、同じパラメータでExpressionをインスタンス化する。
  2. インスタンス化した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;
    }
}

まだ、全てのExpressionのタイプに対応したわけではないけど、やり方はわかった。

DLINQにしてもXLINQにしてもこんな感じでExpressionTreeを解析しSQLやらXPathを構築した後、その結果のみを列挙していく。こうすることで、使う人間からはあたかもオブジェクトに対して条件を与えてフィルタリングやらソートをしているように見せて、その内部ではデータの種類に応じて最適な方法で処理を行う。

そういった事をLINQは拡張メソッドやラムダ式を使って実現している。いや、LINQを実現するために拡張メソッドとラムダ式が作られたのか。

とりあえず終わり、これの全ソースはまたCodePlexででも公開します。