Advent LINQ (13): ExpressionとLINQ

前回、ラムダ式をExpressionクラスとして扱う例を示した。一体これの何がLINQと関係あるのかと思うかもしれない。
ところで、LINQクエリをクエリ構文で書き、それをメソッド構文と対比させて何が違うのかを考えたことがあるだろうか?

// 乱数から偶数だけを抜き出す
var r = new Random();

// クエリ構文
var resultsByQueryStatement =
    from index in Enumerable.Range(0, 10000)
    let randomValue = r.Next()
    where (randomValue % 2) == 0
    select randomValue;

// メソッド構文
var resultsByMethodStatement =
    Enumerable.Range(0, 10000).
    Select(index => r.Next()).
    Where(randomValue => (randomValue % 2) == 0);

どちらも同じ結果が得られる(乱数であることを除けば)。

より具体的にクエリ構文を見てみると、where句の式「(randomValue % 2) == 0」は、メソッド構文のWhere拡張メソッドと対比する事が分かる。但し、メソッド構文ではラムダ式で記述しなければならない。単なる式だけでは、結果はbool値になってしまうからだ。Where拡張メソッドの定義は:

public static class Enumerable
{
    // 第二引数のpredictはデリゲート
    public static IEnumerable<T> Where<T>(
        this IEnumerable<T> enumerable,
        Func<T, bool> predict);
}

となっていて、第二引数(拡張メソッド呼び出しでは、第一引数相当)はデリゲートを渡さなければならない。このデリゲートによって、列挙子enumerableの一つ一つの要素が(呼び出し側のラムダ式によって)評価されて抽出が実現する。式を評価するので、bool値を渡しても意味がないわけだ。

ではここで唐突だが、QueryableクラスのWhereメソッドを見てみよう。

// System.Linq.Queryableクラス(.NET標準クラス)
public static class Queryable
{
    // 第二引数のpredictはExpressionクラス
    public static IEnumerable<T> Where<T>(
        this IQueryable<T> queryable,
        Expression<Func<T, bool>> predictExpression);
}

Queryableクラスは、IQueryableインターフェイスに対するLINQ拡張メソッド群を定義している。このインターフェイスはIEnumerableも継承しているので、通常のLINQクエリ(LINQ to Objects)として使うこともできる。が、普通、LINQ to Objectsを使うときには、このインターフェイスを使わない。どこで使うのだろうか?
その答えが、LINQ to SQLのDataContextや、LINQ to EntitiesのDbContextクラスだ。より正確には、これらのクラス(ウィザードで生成した継承クラス)に定義されている、テーブルを模したプロパティ(AdventureWorksDataEntities.Customerなど)が、IQueryableインターフェイスを実装した型なのだ。だから:

// varを型に置き換えると
using (AdventureWorksLT2012_DataEntities context = new AdventureWorksLT2012_DataEntities())
{
    // LINQクエリ式の結果はIQueryable<Customer>
    IQueryable<Customer> customers =
        from customer in context.Customer   // Customerプロパティは、DbSet<Customer>
        where customer.LastName == "Johnson"
        orderby customer.FirstName descending
        select customer;

    Debug.WriteLine(customers.ToString());
}

CustomerプロパティはDbSet<Customer>クラスだが、このクラスはIQueryable<Customer>を実装している。そのため、以降のクエリは全てQueryableクラスの拡張メソッド群が使用され、最終的な式もIQueryable<Customer>となる。
もう一度、Queryable.Where()の第二引数を見ると、「Expression<Func<T>>」となっている。そのため、where句で書いたラムダ式「customer.LastName == “Johnson”」は、デリゲートとして渡されるのではなく、Expressionとして渡されるのだ。

まとめると、DataContextやDbContextを使用すると、Enumerableクラスの実装ではなく、Queryableクラスの拡張メソッドが呼び出され、ラムダ式で記述した絞り込み条件(他、orderbyやselectの式も同様)は、デリゲートではなくExpressionクラスで渡されることになる。どうしてそうなっているのか?

もう見えてきたかもしれない。Expressionで渡されれば、記述した式そのものの構造を解析する事が可能となる。前回、プロパティを参照するラムダ式をExpressionで取得して、ここからプロパティ名を取得する方法を紹介した。同様に、Whereに渡された絞り込み条件のラムダ式から、絞り込み条件として記述した式の構造そのものにアクセスする事が可能となる。
式で使用したプロパティ名はもちろん、比較式(上記の例では==による文字列との比較や、比較対象の”Johnson”という文字列)も抽出する事が可能だ。もっと複雑な、括弧がネストしたり、複数のプロパティを参照したり、メソッドを呼び出したり、これらを複雑に組み合わせた式も、Expressionから解析する事が出来る。

このような方法で、最終的にExpressionクラスからSQL文を構築し、SQL Serverに送信して実行させているのだ。だから、LINQ to Objectsのように、クエリ結果が大量にインメモリで処理されるという心配は無用だし、基本的にLINQクエリで記述したクエリの効率は、生のSQL文字列で(SqlConnectionやSqlCommandを使って)実行したものと変わらない事になる。
そして、LINQクエリは文字列に頼らないのでタイプセーフだ。前回示したように、リファクタリングツールでも意味が解析されて正しい箇所だけが更新される。SQL Serverのテーブルから生成していれば、エンティティクラスの型定義も間違えようがない。SqlDataReaderでちまちまと型変換しながらレコードを処理する邪魔なコードを書かなくていい。こういった、本来人間がやるべきではない作業を自動化する事が出来るようになる。

投稿者:

kekyo

A strawberry red slime mold. Likes metaprogramming. MA. Bicycle rider. http://amzn.to/1SeuUwD