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を指定するので