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デリゲートに変換するParseExpressionメソッドを呼び出して、そのデリゲートの条件にマッチしたシートのみをXlsWorksheetにラップして列挙するようにしている。

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デリゲートに変換して返している。(これを書いている段階では実装が思いつかなかったorz)

ここで渡ってくるexpressionはPredicateなので、これをPredicateに変換してやるわけだ。こうすることで、XlsWorksheetクラスをインスタンス化する前にフィルタリング処理を実行できる。たいしたもんだ。

ここの変換する処理をいったいどうやって実装すればいいのかと悩んでいたが、よく考えてみればExpressionTreeを解析していって、XlsWorksheetがパラメータとして渡されているところをWorksheetに変えて、それに対するメンバ呼び出しも対応するもの*2に変えて、ExpressionTreeを再構築してやればいいだけだと気がついた。

次はその部分を実装する。しかしLINQを実現するための仕組みはすごいな!

*1:そもそもWhereメソッドはstaticなのででできないわけだが

*2:例えば、NameプロパティはNameプロパティにとか、名前は一緒だけどMemberExpressionではPropertyInfoを指定するので

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