Advent LINQ (18) : Expressionの探索

Expressionは、代入されたラムダ式の構造によっていくらでも変化する。そのため、例で見せたように、あらかじめどの種類のExpressionがやってくるかを予想する事は難しい。そういった構造を解析する方法はいくつか考えられる。単に一つ一つの構造をswitch文のような分岐で処理する事もできるが、標準的な方法として「ExpressionVisitor」クラスが用意されている。
ExpressionVisitorクラスは、Visitorパターンを使用してExpressionを解析する。例えばMethodCallExpressionは、インスタンスを示す「Object」、対象のメンバを示す「Method」、引数群を示す「Arguments」が存在するが、これらもまたExpressionであるので、再帰的に呼び出される。このクラスを継承して、関心のある種類のExpressionのメソッドをオーバーライドしてチェックできる。
特に再帰の入り口となるのが「Visit」メソッドだ。このメソッドの基底の実装が、細分化された「Visit~」メソッドをコールする。試しにVisitをオーバーライドしたのが、以下の例だ。

// ExpressionVisitorクラスを継承する
public sealed class FlatDumpExpressionVisitor : ExpressionVisitor
{
    public FlatDumpExpressionVisitor()
    {
    }

    // 再起処理の入り口となるメソッド
    public override Expression Visit(Expression node)
    {
        // 引数で渡されたExpressionをダンプする
        Debug.WriteLine("{0}: {1}", node.NodeType, node.ToString());
        // デフォルトの実装を呼び出す(細分化されたVisitメソッドを呼び分け、再帰処理する)
        return base.Visit(node);
    }
}

これを実行すると、以下のような出力が得られる。

// ダンプ対象の式を定義
Expression<Func<bool>> func12 = () => "ABCDEFG".StartsWith("ABC") == true;
// ダンプを実行
var visitor = new FlatDumpExpressionVisitor();
visitor.Visit(func12);
// Lambda: () => ("ABCDEFG".StartsWith("ABC") == True)
// Equal: ("ABCDEFG".StartsWith("ABC") == True)
// Call: "ABCDEFG".StartsWith("ABC")
// Constant: "ABCDEFG"
// Constant: "ABC"
// Constant: True

ツリーの階層構造は明確ではないが、ラムダ式から始まって、式の細部を探索していることが読み取れる。また、このFlatDumpExpressionVisitorによって、すべての式の要素がダンプされていそうだ。式中に与えたStartsWithの呼び出しや、固定的な文字列、右辺の「true」も列挙されている。
もう少し、何とかしたい。そこで、以下のようなコードを書いてみる。

public sealed class FlatDumpExpressionVisitor : ExpressionVisitor
{
    private readonly Stack<XElement> elementStack_ = new Stack<XElement>();
    public FlatDumpExpressionVisitor()
    {
    }
    public XElement CurrentElement
    {
        get;
        private set;
    }
    public override Expression Visit(Expression node)
    {
        elementStack_.Push(this.CurrentElement);
        var element = new XElement(node.NodeType.ToString());
        element.Add(new XAttribute("value", node.ToString()));
        if (this.CurrentElement != null)
        {
            this.CurrentElement.Add(element);
        }
        this.CurrentElement = element;
        var result = base.Visit(node);
        this.CurrentElement = elementStack_.Pop() ?? this.CurrentElement;
        return result;
    }
}

これは、再帰的にVisitが呼び出されたときに、XElementを使って階層構造を組み立てるExpressionVisitorの実装だ。まだ式の出力(value属性)が読みにくいが、どのような階層構造になっているのかは把握できる。

<Lambda value="() =&gt; (&quot;ABCDEFG&quot;.StartsWith(&quot;ABC&quot;) == True)">
    <Equal value="(&quot;ABCDEFG&quot;.StartsWith(&quot;ABC&quot;) == True)">
        <Call value="&quot;ABCDEFG&quot;.StartsWith(&quot;ABC&quot;)">
            <Constant value="&quot;ABCDEFG&quot;" />
            <Constant value="&quot;ABC&quot;" />
        </Call>
        <Constant value="True" />
    </Equal>
</Lambda>

ExpressionVisitorクラスには、「VisitBinary」のような細分化されたメソッドが用意されている。これらのノード別メソッドはかなりの種類があるため、すべてを細かく制御するのは骨が折れる。だが、正しく制御すれば、Expressionをより良いXML形式に変換できるだろう。

Advent LINQ (17) : Expressionの評価

ラムダ式をExpressionとして評価するとき、ほんの些細な記述の違いも、Expressionの中には忠実に再現される。string.StartsWithをSQL文に変換する際に、StartsWithの戻り値を明示的に判定する式を書いた。すると、明らかに異なるSQL文が生成された。たとえ意味的に同じ式であっても、Expressionでは異なる表現とみなされる。
以下に、明示的な判定を行わない式のExpressionを確認するコードを示す。

// StartsWithの戻り値をそのまま返す
Expression<Func<bool>> func11 = () => "ABCDEFG".StartsWith("ABC");

// 1.ラムダ式である
var lambdaExpression11 = (LambdaExpression)func11;

// 2.ラムダ式の本体はメソッド呼び出しである
var methodCallExpression11 = (MethodCallExpression)lambdaExpression11.Body;

// 3.メソッド名はStartsWithである
var methodInfo11 = methodCallExpression11.Method;
Debug.Assert(methodInfo11.Name == "StartsWith");

// 4.メソッド呼び出しのインスタンスはリテラル文字列"ABCDEFG"である
var instance11 = (ConstantExpression)methodCallExpression11.Object;
Debug.Assert(instance11.Value.Equals("ABCDEFG"));

// 5.メソッド呼び出しの引数は1個存在する
var arguments11 = methodCallExpression11.Arguments;
Debug.Assert(arguments11.Count == 1);

// 6.その引数の式はリテラル文字列で"ABC"である
var argument011 = (ConstantExpression)arguments11[0];
Debug.Assert(argument011.Value.Equals("ABC"));

ラムダ式からExpressionを得た場合、Expressionは「LambdaExpression」クラスで表現される。このクラスにはラムダ式本体を示す「Body」というプロパティがあり、これがまたExpressionを示している。上では示さなかったが、ReturnTypeやParameters(ラムダ式の引数群)を示すプロパティが定義されている。
次にBodyの中身だが、記述したラムダ式の本体は、string.StartsWithメソッドの呼び出しになっている。なので、Bodyは「MethodCallExpression」クラスで表現されている。対象のメソッドを示す「Method」を見ると、リフレクションのMethodInfoが返される。ここからメソッド名(Name)が”StartsWith”である事が分かる。
string.StartsWithはインスタンスメソッドであるので、「Object」を参照するとインスタンスを特定するExpressionが得られる。この例では”ABCDEFG”というリテラル文字列から直接メソッド呼び出ししているので、「ConstantExpression」にキャストし、「Value」プロパティを確認する事でこの文字列が得られる。
メソッドの引数は「Arguments」で、コレクションが返される。これらの一つ一つの要素もまたExpressionであるので、目的の具象クラスにキャストする必要がある。今回は引数がリテラル文字列であることが分かっているので、「ConstantExpression」にキャストして「Value」にアクセスすると、引数に指定した”ABC”が得られる。
では、明示的に判定を行う式の場合はどうなるだろうか?

// StartsWithの戻り値に、明示的な判定式を記述した場合
Expression<Func<bool>> func12 = () => "ABCDEFG".StartsWith("ABC") == true;

// 1.ラムダ式である
var lambdaExpression12 = (LambdaExpression)func12;

// 2.ラムダ式の本体は二項演算子である
var binaryExpression12 = (BinaryExpression)lambdaExpression12.Body;

// 3.二項演算子の種類はイコールである
Debug.Assert(binaryExpression12.NodeType == ExpressionType.Equal);

// 4.二項演算子の右側はboolリテラル値でtrueである
var rightExpression12 = (ConstantExpression)binaryExpression12.Right;
Debug.Assert(rightExpression12.Value.Equals(true));

// 5.二項演算子の左側はメソッド呼び出しで、メソッド名はStartsWithである
var leftExpression12 = (MethodCallExpression)binaryExpression12.Left;
Debug.Assert(leftExpression12.Method.Name == "StartsWith");

StartsWithの中身は同じなので省略する。つまり、ラムダ式に記述した通りの構造がExpressionに再現される。無意味だからと言って、特に省略されたりすることはない。そして、LINQ to Entitiesではこの違いが、あのようなSQL文の違いとなって表れたと思われる。
同時に、ここまで解析できれば、「メソッド呼び出しの戻り値がboolである」「比較演算子を使用し、右辺(または左辺)が同じ型のリテラル」であれば、式自体を最適化可能であろうことが見えてくる。あとはどこまで実行するかということだ。

Advent LINQ (16): 表形式と構造化形式

SQL文に変換可能かどうかを、もう少し見てみる。以下のやや複雑なLINQクエリは変換できるだろうか?

using (var context = new AdventureWorksLT2012_DataEntities())
{
    // カスタマーとカスタマー住所を内部結合し、カスタマー住所が"Shipping"のものを抽出
    var customer =
        from customer in context.Customer
        join customerAddress in context.CustomerAddress on
            customer.CustomerID equals customerAddress.CustomerID
        where customerAddress.AddressType == "Shipping"
        select new
        {
            Customer = customer,
            CustomerAddress = customerAddress
        };

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

このLINQクエリはもちろん成功し、以下のように変換される。

SELECT
    [Extent1].[CustomerID] AS [CustomerID],
    [Extent1].[NameStyle] AS [NameStyle],
    [Extent1].[Title] AS [Title],
    [Extent1].[FirstName] AS [FirstName],
    [Extent1].[MiddleName] AS [MiddleName],
    [Extent1].[LastName] AS [LastName],
    [Extent1].[Suffix] AS [Suffix],
    [Extent1].[CompanyName] AS [CompanyName],
    [Extent1].[SalesPerson] AS [SalesPerson],
    [Extent1].[EmailAddress] AS [EmailAddress],
    [Extent1].[Phone] AS [Phone],
    [Extent1].[PasswordHash] AS [PasswordHash],
    [Extent1].[PasswordSalt] AS [PasswordSalt],
    [Extent1].[rowguid] AS [rowguid],
    [Extent1].[ModifiedDate] AS [ModifiedDate],
    [Extent2].[CustomerID] AS [CustomerID1],
    [Extent2].[AddressID] AS [AddressID],
    [Extent2].[AddressType] AS [AddressType],
    [Extent2].[rowguid] AS [rowguid1],
    [Extent2].[ModifiedDate] AS [ModifiedDate1]
FROM  [SalesLT].[Customer] AS [Extent1]
INNER JOIN [SalesLT].[CustomerAddress] AS [Extent2] ON
    [Extent1].[CustomerID] = [Extent2].[CustomerID]
WHERE N'Shipping' = [Extent2].[AddressType]

完璧に意図通りだ。しかも、匿名クラスへの射影の概念は.NET CLR固有のものだが、SELECT句でカラムに別名を適用して全て吸い上げている。SQL文には表れないが、結果が返された時点で匿名クラスのそれぞれのプロパティに格納されるのだろう。
LINQクエリのjoinは、内部結合に該当する。残念ながら左・右結合、外部結合に対応する予約語は存在しない。これは同時に、LINQ to Objectsでのjoinにも、そのような演算が無いことを意味する。
但し、そもそも左・右結合は、RDBが「表形式」でしか結果を表現できないために存在するものだと分かれば、大した問題ではない。これらの結合は、対応する値が存在しない場合でも、結果表中の該当するカラムに何らかの値を置く必要があり(そうしないと「表」にならない)、仕方がないので「NULL」を返すのだ。
しかし、LINQの世界は表形式だけではなく、(XMLのように)構造化された要素を自在に扱う事が出来る。だから、そこに結果が存在しな場合はNULLで表現しなくても、はじめから存在しない状態を作り出せる。

using (var context = new AdventureWorksLT2012_DataEntities())
{
    // 製品情報をカテゴリー別に集約する
    var productsByProductCategory =
        from product in context.Product
        group product by product.ProductCategoryID;

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

LINQクエリの「group ~ by」は、名称からSQL分のGROUP BY句を彷彿とさせる。実際、レコード結果をセレクタに従って集約するという点では、GROUP BY句と同じように動作する。
LINQ to Objectsであれば、IGrouping<T, U>インターフェイスの列挙子が返される。このインターフェイスは、IDictionary<T, U>が構造化されたような形をしている。つまり、一つのキー値(T)に対して、n個の要素値(IEnumerable<U>)が格納される。
IDictionaryが「表形式」と考えるなら、IGrouping(IGroupingの列挙子)は「構造化形式」だ。
ただ、得られるSQL文は分かりにくい。

SELECT
    [Project2].[C1] AS [C1],
    [Project2].[ProductCategoryID] AS [ProductCategoryID],
    [Project2].[C2] AS [C2],
    [Project2].[ProductID] AS [ProductID],
    [Project2].[Name] AS [Name],
    [Project2].[ProductNumber] AS [ProductNumber],
    [Project2].[Color] AS [Color],
    [Project2].[StandardCost] AS [StandardCost],
    [Project2].[ListPrice] AS [ListPrice],
    [Project2].[Size] AS [Size],
    [Project2].[Weight] AS [Weight],
    [Project2].[ProductCategoryID1] AS [ProductCategoryID1],
    [Project2].[ProductModelID] AS [ProductModelID],
    [Project2].[SellStartDate] AS [SellStartDate],
    [Project2].[SellEndDate] AS [SellEndDate],
    [Project2].[DiscontinuedDate] AS [DiscontinuedDate],
    [Project2].[ThumbNailPhoto] AS [ThumbNailPhoto],
    [Project2].[ThumbnailPhotoFileName] AS [ThumbnailPhotoFileName],
    [Project2].[rowguid] AS [rowguid],
    [Project2].[ModifiedDate] AS [ModifiedDate]
FROM ( SELECT
    [Distinct1].[ProductCategoryID] AS [ProductCategoryID],
    1 AS [C1],
    [Extent2].[ProductID] AS [ProductID],
    [Extent2].[Name] AS [Name],
    [Extent2].[ProductNumber] AS [ProductNumber],
    [Extent2].[Color] AS [Color],
    [Extent2].[StandardCost] AS [StandardCost],
    [Extent2].[ListPrice] AS [ListPrice],
    [Extent2].[Size] AS [Size],
    [Extent2].[Weight] AS [Weight],
    [Extent2].[ProductCategoryID] AS [ProductCategoryID1],
    [Extent2].[ProductModelID] AS [ProductModelID],
    [Extent2].[SellStartDate] AS [SellStartDate],
    [Extent2].[SellEndDate] AS [SellEndDate],
    [Extent2].[DiscontinuedDate] AS [DiscontinuedDate],
    [Extent2].[ThumbNailPhoto] AS [ThumbNailPhoto],
    [Extent2].[ThumbnailPhotoFileName] AS [ThumbnailPhotoFileName],
    [Extent2].[rowguid] AS [rowguid],
    [Extent2].[ModifiedDate] AS [ModifiedDate],
    CASE WHEN ([Extent2].[ProductID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C2]
    FROM   (SELECT DISTINCT
        [Extent1].[ProductCategoryID] AS [ProductCategoryID]
        FROM [SalesLT].[Product] AS [Extent1] ) AS [Distinct1]
    LEFT OUTER JOIN [SalesLT].[Product] AS [Extent2] ON
        ([Distinct1].[ProductCategoryID] = [Extent2].[ProductCategoryID]) OR
        (([Distinct1].[ProductCategoryID] IS NULL) AND ([Extent2].[ProductCategoryID] IS NULL))
    )  AS [Project2]
ORDER BY [Project2].[ProductCategoryID] ASC, [Project2].[C2] ASC

サブクエリバリバリな、読みにくいSQL文が出力された。

  1. ProductCategoryIDをDISTINCTしたリストを作る(一番内側のクエリ)
    つまり、カテゴリーIDの重複のないリストを作る。
  2. 次に、そのリストを使って左結合でCustomerAddressを抽出する(LEFT OUTER JOINのクエリ)
    ここでC1とC2という付加情報を加えている。C1は1固定なので用途が分からないが、C2はカテゴリーに該当するレコードが存在したかどうかを表しているように見える。
  3. 最後に、結合結果をカテゴリーIDとC2でソートする。

このようなSQL文となる理由はあまり思いつかず、はっきりとした理由は不明だ。最終的にカテゴリーID毎に集約しなければならないため、カテゴリーIDでソートするのは理に適っているように思える(逆シリアル化してインスタンスに格納するときに、ソートされていれば順序実行すれば良くなる。件数が莫大かもしれないため、返却後にインメモリでソートしない方が良いだろう)。
何れにしても、このSQL文はLINQ to Entitiesのバージョンが上がる時に変更される可能性があるので深入りしないが、重要なのは、RDBとのやりとりはどうしても「表形式」とするしかないが、結果が戻ってきたら、「構造化形式」に集約出来ると言う事だ。構造化されていれば、結合結果としてカラムチェックに頼らなければならないような、書きにくいコードから解放される。

using (var context = new AdventureWorksLT2012_DataEntities())
{
    // 製品情報をカテゴリー別に集約する
    var productsByProductCategory =
        from product in context.Product
        group product by product.ProductCategoryID;

    // カテゴリー別に製品情報を出力する
    foreach (var categorizedProducts in productsByProductCategory)
    {
        Console.WriteLine("CategoryID: {0}", categorizedProducts.Key);
        foreach (var product in categorizedProducts)
        {
            Console.WriteLine("   Product: {0}, {1}, {2}",
                product.ProductNumber,
                product.Name,
                product.ListPrice);
        }
    }
}

もちろん、LINQクエリなので、パイプライン実行される。SQL Server上ではORDER BYが実行されるので一時リソースを消費するが、.NET側では結果のレコード群が逐次処理される。最終出力はConsoleに出しているだけなので、結果が何千万行あってもメモリは圧迫されない。
今まで苦労してきた開発手法は一体何だったのか、と思わずにはいられないかもしれない。集約の簡便さもさることながら、コードを一瞥しただけで処理内容が把握できるため、保守も容易だ。このコードにはforeach以外に制御構文が存在しない。サイクロマティック複雑度は十分に小さい。それはつまり、テストに掛かるコストも小さいと言う事だ。
最後に、結果をXMLに変換するのに、制御構造が不要である例を示そう。

using (var context = new AdventureWorksLT2012_DataEntities())
{
    // 製品情報をカテゴリー別に集約する
    var productsByProductCategory =
        from product in context.Product
        group product by product.ProductCategoryID;

    // カテゴリー別に製品情報をXMLで出力する
    var categoriesXml = new XElement("Categories",
        from categorizedProducts in productsByProductCategory.AsEnumerable()   // <-- ゲート
        select new XElement(
            "Category",
            new XAttribute("id", categorizedProducts.Key),
            from product in categorizedProducts
            select new XElement("Product",
                new XAttribute("name", product.Name),
                new XAttribute("number", product.ProductNumber),
                new XAttribute("price", product.ListPrice))));

    Debug.WriteLine(categoriesXml);
}

「AsEnumerable」でLINQ to EntitiesとLINQ to XMLの間にゲートを作っていることに注意。これがないと、LINQ to XMLのクエリがLINQ to Entitiesの一部とみなされ、SQL文に変換しようとして失敗する。
このコードのユニットテストの想像が付くだろうか? 従来の制御構文によるロジックバリバリのコードのユニットテストと異なり、このコードのテストは純粋なインターフェイス境界のテストとして非常に設計しやすいはずだ。この事を知っていれば、機能設計の段階から設計方針が異なってくる。
(但し、ユニットテストとして成立させるためには、DataContextを分離する必要がある事に注意。このままではデータベースが存在しないとテスト出来ないので良くない)。

Advent LINQ (15): SQL表現可能な式

前回、LINQ to SQLまたはLINQ to Entitiesは、どんなラムダ式でもSQLに変換できるわけではない事を示した。では、どのような式なら変換できるのだろうか?
例えば、stringクラスにはStartsWithメソッドがある。これを使って:

using (var context = new AdventureWorksLT2012_DataEntities())
{
    // Johnで始まる苗字のレコードを抽出
    var customer0 =
        from customer in context.Customer
        where customer.LastName.StartsWith("John")
        select customer;

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

実行すると、エラーとはならない。そして以下のSQL文が出力される。

SELECT
    [Extent1].[CustomerID] AS [CustomerID],
    [Extent1].[NameStyle] AS [NameStyle],
    [Extent1].[Title] AS [Title],
    [Extent1].[FirstName] AS [FirstName],
    [Extent1].[MiddleName] AS [MiddleName],
    [Extent1].[LastName] AS [LastName],
    [Extent1].[Suffix] AS [Suffix],
    [Extent1].[CompanyName] AS [CompanyName],
    [Extent1].[SalesPerson] AS [SalesPerson],
    [Extent1].[EmailAddress] AS [EmailAddress],
    [Extent1].[Phone] AS [Phone],
    [Extent1].[PasswordHash] AS [PasswordHash],
    [Extent1].[PasswordSalt] AS [PasswordSalt],
    [Extent1].[rowguid] AS [rowguid],
    [Extent1].[ModifiedDate] AS [ModifiedDate]
FROM [SalesLT].[Customer] AS [Extent1]
WHERE [Extent1].[LastName] LIKE N'John%'

SQL文にはStartsWithメソッドなど存在しない。だからエラーになるかと思いきや、LIKE句に置き換わって、結果として意図通りの抽出が実行されるSQL文が組み立てられた。
これは明らかに、string.StartsWithメソッドを認識し、等価なLIKE句に置き換えていることが伺える。つまり、与えられたExpressionを解析し、その式の中にStartsWithメソッドの呼び出しが含まれていれば、LIKE句を組み立ててSQL文に追加する、と言う事をしているのだろうと予想できる。それならば、EndsWithも同じようにLIKE句に置き換えられるはずだ。実際に試して確認してほしい。
なお、以下のような式を記述すると、生成されるSQL文も変化する。

using (var context = new AdventureWorksLT2012_DataEntities())
{
    // Johnで始まる苗字のレコードを抽出
    var customer1 =
        from customer in context.Customer
        where customer.LastName.StartsWith("John") == true   // <-- 明示的にtrueで判定
        select customer;

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

生成されるSQL文はこうだ:

SELECT
    [Extent1].[CustomerID] AS [CustomerID],
    [Extent1].[NameStyle] AS [NameStyle],
    [Extent1].[Title] AS [Title],
    [Extent1].[FirstName] AS [FirstName],
    [Extent1].[MiddleName] AS [MiddleName],
    [Extent1].[LastName] AS [LastName],
    [Extent1].[Suffix] AS [Suffix],
    [Extent1].[CompanyName] AS [CompanyName],
    [Extent1].[SalesPerson] AS [SalesPerson],
    [Extent1].[EmailAddress] AS [EmailAddress],
    [Extent1].[Phone] AS [Phone],
    [Extent1].[PasswordHash] AS [PasswordHash],
    [Extent1].[PasswordSalt] AS [PasswordSalt],
    [Extent1].[rowguid] AS [rowguid],
    [Extent1].[ModifiedDate] AS [ModifiedDate]
FROM [SalesLT].[Customer] AS [Extent1]
WHERE (1 = (CASE WHEN ([Extent1].[LastName] LIKE N'John%') THEN cast(1 as bit) WHEN ( NOT ([Extent1].[LastName] LIKE N'John%')) THEN cast(0 as bit) END)) AND (CASE WHEN ([Extent1].[LastName] LIKE N'John%') THEN cast(1 as bit) WHEN ( NOT ([Extent1].[LastName] LIKE N'John%')) THEN cast(0 as bit) END IS NOT NULL)

かなりのクソ真面目ぶりだ。SQL Serverのクエリオプティマイザーがこの式をどう評価するか分からないが、LINQクエリを記述する際は、それがどのようなSQL文として実行されるのかは気にかけておいた方が良い。書き方一つでこれだけ変わってしまうのは、人によってはLINQクエリで書く事に抵抗を感じるかもしれない。しかし、長い目で見れば、新しい.NET FrameworkではExpressionの評価次第でシンプルなクエリに変換されるようになるかもしれないし、タイプセーフである事は変わらない。
他にも特別に解釈され、SQL文にマッピングされるLINQ拡張メソッド群が存在する。Sum, Count, Average, Min, Maxといった集計を行うメソッドは、対応するSQL文に変換可能だ。また、Skip, Takeメソッドを使って、抽出結果の部分レコード抽出も可能だ。これらはOFFSET句とFETCH句に変換される。

Advent LINQ (14): 条件式が実行される場所

前回、.NETにはQueryableクラスがあり、Enumerableクラスの代わりに使用されることで、クエリの条件式をExpressionクラス経由で動的に判断できることを示した。
Queryableクラスには、Enumerableクラスで定義されている様々な拡張メソッドが大体同じように定義されている。今までの連載で、LINQ 拡張メソッドの実装方法例を数多く紹介してきた。Queryableクラスの拡張メソッドは、IQueryableを拡張するという違いを除けば、IEnumerableの拡張メソッドと殆ど変わる事は無い。

本当にそうだろうか?

まず第一に、Funcなどのデリゲートで受けていた条件式は、Expressionクラスで受ける必要があるはずだ。そして、例えば拡張する対象がLINQ to SQLなら、SQL文として成立させなければならない。独自の拡張メソッドを、IQueryableインターフェイス向けに実装したとしよう:

public static class LinqExtensions
{
    // 要素の個数を数える(LINQ to Objects)
    public static IEnumerable<T> QCount<T>(
        this IEnumerable<T> enumerable,
        Func<T, bool> predict)
    {
        var count = 0;
        foreach (var value in enumerable)
        {
            // 条件が一致する場合だけカウントする
            if (predict(value) == true)
            {
                count++;
            }
        }
        return count;
    }

    // 要素の個数を数える(LINQ to SQL / LINQ to Entities)
    public static IQueryable<T> QCount<T>(
        this IQueryable<T> queryable,
        Expression<Func<T, bool>> predict)
    {
        var count = 0;
        foreach (var value in queryable)
        {
            // 条件が一致する場合だけカウントする
            if (predict(value) == true)     // <-- 構文エラー
            {
                count++;
            }
        }
        return count;
    }
}

このコード例には2つの問題がある。第一に、IQueryableのバージョンでは、要素数を数える処理がSQL文になっていないことだ。LINQ to SQLやLINQ to Entitiesでは、クエリの結果がSQL文としてSQL Server等のRDBシステム上で実行されなければならないが、この実装は手元のCLR上でループを実行しているだけで、見ての通りSQL文はどこにも存在しない。
第二に、条件式を示すpredictがデリゲートではなくExpressionであるため、直接呼び出すことは出来ない(構文エラーとなる)。Expressionは自分で解析して、式がどのようになっているかを把握しなければならない。このQCountをSQL文で表現したとすると、以下のようになるはずだ。

// QCountがSQL文に変換されたとして想定される、擬似的なSQL文
SELECT COUNT(*) FROM [Customer] WHERE predict(row) = TRUE

この擬似SQL文を見ると更に大きな問題がある事が分かる。このSQL文がSQL Serverに送信されて実行される時、predictの呼び出しで、どこのメソッドが呼び出されるのだろうか?.NET側に記述したラムダ式が実行されるのだろうか?SQL Serverからのリモートコールバックで実行されるのか?それはそれは壮大な仕掛けだ。

… そんなわけはない。
第一、仮にそのように動作したとしても、毎行のWHERE評価の度にコールバック呼び出しが発生する事になる。非現実的な遅さだ。では実際にはどこで実行されるのだろうか?
この答えは、「そもそも想定が間違っている」だ。まず、predictはデリゲートではないので、Expressionを自力で評価する必要がある。「評価した式がSQL文に変換可能」であれば、SQL文としてRDBに送信出来ることになる。変換不能であれば、クエリを実行する事は出来ない。それは「実行時例外」という形で表現されることになる。例を示そう:

// 正規表現でマッチングを試みる
var regex = new Regex("[a-zA-Z]*");

// LINQ to SQLのコンテキスト
using (var context = new AdventureWorksDataContext())
{
    // LINQ to Objectsなら問題ないが?
    var customers =
        from customer in context.Customer
        where regex.IsMatch(customer.FirstName) == true // 正規表現にマッチすれば
        select customer;
}

Regexクラスは、当然.NET固有のクラスだ。Regex.IsMatchに相当するSQL構文は存在しない。だからこのLINQクエリは、SQL文で「表現できない」。表現できないので、実行しようとすると例外が発生する。
つまり、IQueryableの拡張メソッドは、そのメソッドが意味するところをSQL文に変換出来なければならない。或は逆だ。SQL文で表現可能であれば、その意味するところを拡張メソッドに出来る可能性がある、と。

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でちまちまと型変換しながらレコードを処理する邪魔なコードを書かなくていい。こういった、本来人間がやるべきではない作業を自動化する事が出来るようになる。

Advent LINQ (12): リファクタリング可能なメンバ名

デバッグやフレームワークのサポートメソッドとして、動的にプロパティ名を取得したい場合がある。例えば、エラーが発生した場合に、そのエラーが発生したプロパティ名をログに記録したいとする。

private ILog log_ = ...;
private string description_;
// 詳細内容を取得・設定する(スペースは含まれてはならない)
public string Description
{
    get
    {
        return description_;
    }
    set
    {
        // スペースが含まれていれば
        if (value.Contains(" ") == true)
        {
            // ログにエラーを記憶する(Log4Net風)
            log_.Error("Description");        // <-- プロパティ名
            return;
        }
        description_ = value;
    }
}

ログ出力時に、プロパティ名を文字列として渡している。このコードを実装後、「Description」というプロパティ名が不適切であり、「Title」に変更したいとしよう。Visual Studioにはテキスト一括置換が存在するので、DescriptionからTitleに一括置換する方法が考えられる。しかし、この方法は他のコードでたまたまDescriptionという名前を持つプロパティやメソッド、更にはDescriptionという文字列なども含め、あらゆるDescriptionをTitleに変更してしまう。これでは、正しく置換されたかどうかを検証しなければならない。

そのようなわけで、Visual Studioには「リファクタ」機能があり、安全にシンボル名を変更する事が出来る。変更しようとしているシンボル名を意味的に解析し、単に同じ名前というだけでなく、実際にコードとして同じ対象を参照するシンボル名だけを、正しく置換してくれる。この機能を使って、上記のDescriptionをTitleに置き換えると、以下のようになる。

public sealed class PersonEntity
{
    private ILog log_ = ...;
    private string title_;      // <-- title_に変更

    // Titleに変更
    public string Title
    {
        get
        {
            return title_;
        }
        set
        {
            // スペースが含まれていれば
            if (value.Contains(" ") == true)
            {
                // ログにエラーを記憶する(Log4Net風)
                log_.Error("Description");        // <-- プロパティ名
                return;
            }
            title_ = value;
        }
    }
}

最初にプライベートフィールドの「description_」を「title_」にリファクタする。すると、ソースコード上のすべてのdescriotion_がtitle_に置き換わる。一括置換でも同じ結果が得られるが、こちらの方がより安全に修正出来ることは、上で説明したとおりだ。
次に、プロパティの名前「Description」を「Title」にリファクタする。ここで問題が発生する。Descriptionプロパティを参照している箇所(上記には存在しないが、他のメソッドやクラスなどから参照している場合は、それらのシンボル名)は、リファクタ操作で正しく修正される。しかし、ログ出力を行っているErrorメソッドの引数の”Description”という文字列は修正されない。

リファクタ操作では、コードの意味が解析される。プロパティやメソッドへの参照コードであれば、自動的に追跡されて修正されるが、文字列である場合は意味的に同一なのかどうかを機械的に判定する事が出来ない。そのため、リファクタでは文字列は置換対象とならないのだ。

このようなコードが実装の中に散見するようになると、リファクタ機能を使って積極的に安全に修正を行うことが困難となる。そこで、コードに意味を与え、リファクタ対象のシンボル名として認識させつつも、動的にシンボル名を取得する方法を考える必要がある。
まず、ログ出力のコードを独立したメソッドにする。

// ログを出力する
private void WriteErrorLog(string propertyName)
{
    log_.Error(propertyName);
}
public string Title
{
    get
    {
        return title_;
    }
    set
    {
        // スペースが含まれていれば
        if (value.Contains(" ") == true)
        {
            // ログにエラーを記憶する
            this.WriteErrorLog("Description");        // <-- プロパティ名
            return;
        }
        title_ = value;
    }
}

やっていることは変わらない。次が肝心だ。WriteErrorLogの引数を、以下のように変更する。

// ログを出力する(タイプセーフ版)
private void WriteErrorLog<T>(Expression<Func<PersonEntity, T>> lambdaExpression)
{
    log_.Error(/* TODO: propertyName */);
}

Expressionクラスを受け取るようになっている。Expressionクラスとは、「式」の構造を表したインスタンスだ。例えば、このメソッドは以下のように使用する。

public string Title
{
    get
    {
        return title_;
    }
    set
    {
        // スペースが含まれていれば
        if (value.Contains(" ") == true)
        {
            // ログにエラーを記憶する
            this.WriteErrorLog(this_ => this_.Title);      // <-- プロパティを参照するラムダ式
            return;
        }
        title_ = value;
    }
}

WriteErrorLogにラムダ式を記述している。ラムダ式は暗黙の型変換が成立する場合は、デリゲートに置き換えることができる。従って、通常は以下のようなデリゲート型(Func<T, U>)の変数で受ける。

// 匿名デリゲート(C# 2.0)
Func<PersonEntity, string> propertyNameFunc = delegate() { return this_.Title; }

// ラムダ式(C# 3.0)
Func<PersonEntity, string> propertyNameFunc = this_ => this_.Title;

しかし、ラムダ式で記述している場合に限り、Expressionクラスでも受けることができる。

// ラムダ式ツリー(C# 3.0)
Expression<Func<PersonEntity, string>> lambdaExpression = this_ => this_.Title;

表面的には、Expressionクラスで受けていること以外に違いはない。しかし、Expressionクラスは以下のようにデリゲートを実行する事が出来ない(そもそもExpressionクラスはデリゲートではない)。

// 匿名デリゲート(C# 2.0)
var title = propertyNameFunc();

// ラムダ式(C# 3.0)
var title = propertyNameFunc();

// ラムダ式ツリー(C# 3.0)
var title = lambdaExpression(); // <-- 構文エラー(デリゲートではない)

Expressionクラスが何者なのかはさておき、このクラスを以下のようにすると、ラムダ式に記述したプロパティ名を取得する事が出来る。

// ログを出力する(タイプセーフ版)
private void WriteErrorLog<T>(Expression<Func<PersonEntity, T>> lambdaExpression)
{
    // ラムダ式の式部分を、メンバ照会式として取得する
    var memberExpression = (MemberExpression)lambdaExpression.Body;

    // メンバ照会式が参照するメンバ情報を、プロパティ情報として取得する
    var propertyInfo = (PropertyInfo)memberExpression.Member;

    // プロパティ名を取得する
    log_.Error(propertyInfo.Name);
}

これで、WriteErrorLogメソッドの引数をラムダ式で記述可能となった。ラムダ式でプロパティ参照式を書けば、それはコードとして意味のある記述となる。コードの意味が解釈できれば、リファクタ機能の対象として、自動的に修正が可能となる。文字列による緩すぎるシンボル名の参照を排除する事で、コードのメンテナンス性が向上する。

Advent LINQ (11): SQL文を取得する

LINQ to SQLやLINQ to Entitiesは、記述したLINQクエリが直接SQL文に変換されて、SQL ServerなどのRDBシステムに発行される。今まで見て来たLINQクエリや拡張メソッドは、LINQ to Objectsと呼ばれ、同じLINQでありながら、その動作は全く異なる。
もし、連載で示してきた独自の拡張メソッドがそのままSQL Serverと連携するなら、以下のような矛盾が考えられる。

  • 独自に記述した拡張メソッドが、ifやwhereなどの「分岐ロジック」を含むとしたら、それはどうやってSQL Server上で動作するのか?
  • 逆に、独自の拡張メソッドが不可能だとしたら、単にSQL Serverからレコードをフェッチして、実際の絞り込み(Where)や並べ替え(OrderBy)は、.NET CLR上でインメモリで行われるのか? そうだとするなら、何百万行もあるレコードをCLR上に読み込むこと自体非現実的で、LINQ to SQLやLINQ to Entitiesに実用性は無いのでは?

LINQ to SQLやLINQ to Entitiesは、もちろん「SQL構文」でクエリを生成し、SQL Server上でクエリが実行され、「その結果だけ」がCLRに返される。LINQ to Objectsだけを知っている状態では、この動作はにわかに信じがたいかもしれない。また、どうしてそのような事が実現出来るのかは、とてもこの連載だけでは説明できないが、とりあえず、実際に生成されるSQL文を確認する事は出来る。
SQL文の確認方法は、LINQ to SQLとLINQ to Entitiesで異なるため、それぞれについて例を示す。
サンプルDB: AdventureWorks LT 2012
LINQ to SQLの場合、データベース接続から「LINQ to SQLクラス」を生成する。すると、dbmlファイルのデザイナーが表示される(中身は空)。
LINQToSQL1
ここに、サーバーエクスプローラーから必要なテーブルをドラッグアンドドロップで落とすと、以下のようにテーブルが表示される(実際はEntityクラス)。
LINQToSQL2
テーブル間に正しく制約条件が定義されていれば、このようにリレーションシップ(矢印)も自動的に定義される。見た目だけでなく、Entitiyクラスの階層構造が自動的にプロパティとして定義されるので、LINQ to SQLやLINQ to Entitiesを使うなら、制約条件を定義する事は重要な作業となる。
データベースファーストな開発であれば、SQL Server Management Studioで、テーブル設計と同時に制約もつければよい。リリースで邪魔なら、最後に制約だけ削除する方法もある。
これで、データベース接続を抽象化するDataContextクラスと、レコードデータを表すEnttiyクラスが生成された。

// dbmlで定義されたDataContextを生成する
using (var context = new AdventureWorksDataContext())
{
    // Customerテーブルから、LastNameが"Johnson"のレコードを抽出し、FirstNameの降順でソートする
    var customers =
        from customer in context.Customer
        where customer.LastName == "Johnson"
        orderby customer.FirstName descending
        select customer;

    // 上記クエリの、実際のSQL文を取得する
    var sqlText = customers.ToString();
    Debug.WriteLine(sqlText);
}

この結果、以下のSQL文が得られる。

SELECT
    [t0].[CustomerID], [t0].[NameStyle], [t0].[Title], [t0].[FirstName],
    [t0].[MiddleName], [t0].[LastName], [t0].[Suffix], [t0].[CompanyName],
    [t0].[SalesPerson], [t0].[EmailAddress], [t0].[Phone], [t0].[PasswordHash],
    [t0].[PasswordSalt], [t0].[rowguid], [t0].[ModifiedDate]
FROM [SalesLT].[Customer] AS [t0]
WHERE [t0].[LastName] = @p0
ORDER BY [t0].[FirstName] DESC

LINQのfrom句やselect句はもちろんだが、where句で記述した絞り込み条件(”Johnson”はパラメータ化されている)やorderby句で記述したソート条件(もちろん、descendingも)反映されている。
LINQ to Entitiesの場合、「ADO.NET Entity Data Model」から生成する。
LINQToEntities1
LINQToEntities2
LINQToEntities3
使い方はほぼ同等。LINQクエリ自体は全く同じ。

using (var context = new AdventureWorksLT2012_DataEntities())
{
    var customers =
        from customer in context.Customer
        where customer.LastName == "Johnson"
        orderby customer.FirstName descending
        select customer;

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

以下が出力されたSQL句。

SELECT
    [Extent1].[CustomerID] AS [CustomerID],
    [Extent1].[NameStyle] AS [NameStyle],
    [Extent1].[Title] AS [Title],
    [Extent1].[FirstName] AS [FirstName],
    [Extent1].[MiddleName] AS [MiddleName],
    [Extent1].[LastName] AS [LastName],
    [Extent1].[Suffix] AS [Suffix],
    [Extent1].[CompanyName] AS [CompanyName],
    [Extent1].[SalesPerson] AS [SalesPerson],
    [Extent1].[EmailAddress] AS [EmailAddress],
    [Extent1].[Phone] AS [Phone],
    [Extent1].[PasswordHash] AS [PasswordHash],
    [Extent1].[PasswordSalt] AS [PasswordSalt],
    [Extent1].[rowguid] AS [rowguid],
    [Extent1].[ModifiedDate] AS [ModifiedDate]
FROM [SalesLT].[Customer] AS [Extent1]
WHERE N'Johnson' = [Extent1].[LastName]
ORDER BY [Extent1].[FirstName] DESC

もし、これらのSQL文が本当に実行されているのか疑問であるなら、SQL Server Profilerで実際に確認してみると良い。

Advent LINQ (10): .NET 2.0でLINQを使う

LINQは、正式には.NET 3.5 / C# 3.0でサポートされた。しかし、強力な柔軟性と拡張性の高さを、.NET 2.0のプロジェクトでも使いたいと思う事がある。実は、工夫さえすれば、.NET 2.0環境でもLINQを使う事が出来る。
LINQを使うためには、2つの要件を満たす必要がある。

  • C#コンパイラが、LINQクエリ構文を解釈出来るようにする。
    これを満たすためには、C# 3.0以上のコンパイラを使用すればよい。つまり、Visual Studio 2008以上の環境を使えば、コンパイラはfrom・where・select・orderbyなどのLINQクエリ構文や、ラムダ式を解釈可能になる。例え、コンパイラのターゲットが.NET 2.0に設定されていても、これらの構文は解釈可能だ。
  • LINQの拡張メソッド群(Enumerableクラスなど)が必要。
    これは当然、.NET 2.0のmscorlib.dllやSystem.dllには含まれていない。従って、何とかして用意する必要がある。

前者の要件に絡む、分かりにくい問題がある。「拡張メソッド」の扱いだ。C# 3.0以上のコンパイラを使えば、拡張メソッド構文は解釈可能だ。但し、拡張メソッドが定義されたクラスは特殊な扱いを受ける。

// 独自実装のEnumerable
[Extension] // <-- Extension属性
public static class Enumerable
{
    // 独自実装のWhere
    [Extension] // <-- Extension属性
    public static IEnumerable<T> Where<T>(this IEnumerable<T> enumerable, Func<T, bool> predict)
    {
        foreach (var value in enumerable)
        {
            if (predict(value) == true)
            {
                yield return value;
            }
        }
    }
}

C# 3.0以上のコンパイラで拡張メソッドを定義すると、暗黙にExtensionAttribute属性がクラスとメソッドに適用される(上の例ではわざと書いた)。そのため、アセンブリを生成する際にこの拡張メソッドが見つからないと、ビルドに失敗する。当然、.NET 2.0のmscorlib.dllやSystem.dllには存在しないため、以下のようなコードも盛り込んでおく必要がある。

namespace System.Runtime.CompilerServices
{
    // オレオレExtension属性
    [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)]
    public sealed class ExtensionAttribute : Attribute
    {
        public ExtensionAttribute()
        {
        }
    }
}

そして、こまごまとしたクラス(例えば、Func<T>やAction<T>など)と、あなたが使いたい標準的なLINQ拡張メソッド群(Where<T>やSelect<T>やOrderBy<T>等)を用意する必要がある。これらを用意すれば、「ほぼ」C# 3.5以降のLINQを模倣できる。
ほぼ、と書いたのは、どうしても回避出来ない制限が一つだけあるためだ。それは、IEnumerable<T>が、ジェネリック共変性をサポートしていない事による。これによって発生する問題は別の機会に譲るとして、実際問題、大量のLINQ拡張メソッドを自前で準備するのはなかなか難しい。

そして、同じような事を考える人は世の中にも大勢いて、NuGetでパッケージ化されていたりもするので、特に理由が無ければ、このようなパッケージを利用したほうが良いだろう。以下の「linqbridge」は、単一ソースコードのバージョンもあるので、自分のライブラリに容易に組み込みやすいだろう。

linqbridge – Re-implementation of LINQ to Objects for .NET Framework 2.0
NuGet LINQBridge
NuGet LINQBridge (Embedded)

なお、linqbridgeはPLINQをサポートしていない。AsParallel()から始まるPLINQを用意するのは複雑すぎる。自前で実装するのは不可能ではないが、そんな事を考えるならC# 3.0に移行する事を真剣に考えた方が良い。どうしても並列実行を諦めることが出来ないのなら、TPLの自前実装で我慢しよう。Palallel.ForEach()なら、自力でも実装できるはずだ。

Advent LINQ (9): ParallelQuery対応拡張メソッドの実装

LINQクエリ中で使用出来る拡張メソッドを実装するとき、その引数シグネチャはIEnumerable<T>で受ける事になる。

public static class LinqExtensions
{
    // パブリックなデフォルトコンストラクタを持つ型を抽出する
    public static IEnumerable<Type> OfCreatable(this IEnumerable<Type> enumerable)
    {
        return
            from type in enumerable
            where
                (type.IsPublic == true) &&  // パブリックであり、
                (type.IsClass == true) &&   // クラスであり、
                (type.IsAbstract == false) &&   // 抽象クラスではなく
                (type.GetConstructor(Type.EmptyTypes) != null)  // パブリックなデフォルトコンストラクタがある
            select type;
    }
}

この拡張メソッドを使ってみる。

// mscorlib.dllのすべての型から、生成可能なクラスを抽出する
var types = typeof(object).Assembly.GetTypes();
var creatables = types.OfCreatable();

この時、typesはType[]なので、引数のIEnumerable<Type>に合致する。OfCreatableに実装したLINQクエリは以下のように拡張メソッドで書き直せる。

public static class LinqExtensions
{
    // パブリックなデフォルトコンストラクタを持つ型を抽出する
    public static IEnumerable<Type> OfCreatable(this IEnumerable<Type> enumerable)
    {
        return
            enumerable.Where(type =>
                (type.IsPublic == true) &&  // パブリックであり、
                (type.IsClass == true) &&   // クラスであり、
                (type.IsAbstract == false) &&   // 抽象クラスではなく
                (type.GetConstructor(Type.EmptyTypes) != null));    // パブリックなデフォルトコンストラクタがある
    }
}

ここで、typesを並列化したらどうなるだろうか。

// mscorlibのすべての型から、生成可能なクラスを抽出する
var types = typeof(object).Assembly.GetTypes().AsParallel();    // 並列化
var creatables = types.OfCreatable();

varを書き直すと、以下のようになる。

// mscorlib.dllのすべての型から、生成可能なクラスを抽出する
ParallelQuery<Type> types = typeof(object).Assembly.GetTypes().AsParallel();  // 並列化
IEnumerable<Type> creatables = types.OfCreatable();

AsParallel()によって、列挙子の方がParallelQuery<Type>となる。しかし、OfCreatableの引数はIEnumerable<Type>なので、暗黙のキャストが発生し、結局OfCreatable内のLINQクエリは並列化されないまま実行される(以前の連載で述べたように、これは暗黙のゲートだ)。念のため、こういうことだ:

// mscorlib.dllのすべての型から、生成可能なクラスを抽出する
ParallelQuery<Type> types = typeof(object).Assembly.GetTypes().AsParallel();  // 並列化
IEnumerable<Type> creatables = LinqExtensions.OfCreatable((IEnumerable<Type>)types);    // IEnumerable<Type>に戻してから渡される

では、OfCreatable内のLINQクエリを並列化したい場合はどうすれば良いだろうか?

public static class LinqExtensions
{
    // パブリックなデフォルトコンストラクタを持つ型を抽出する
    public static IEnumerable<Type> OfCreatable(this IEnumerable<Type> enumerable)
    {
        return
            from type in enumerable.AsParallel()    // <-- ここで並列化
            where
                (type.IsPublic == true) &&  // パブリックであり、
                (type.IsClass == true) &&   // クラスであり、
                (type.IsAbstract == false) &&   // 抽象クラスではなく
                (type.GetConstructor(Type.EmptyTypes) != null)  // パブリックなデフォルトコンストラクタがある
            select type;
    }
}

並列化したいのだから、内部のLINQクエリでもAsParallel()で並列化すればよいと思うかもしれないが、これは微妙だ。引数のところでゲートが出来ていることは解消されない(IEnumerable<Type>になる)ため、OfCreatableを呼び出す直前のクエリが並列クエリであったとしても、一旦並列性が解除されてしまう。
また、戻り値として返される列挙子もIEnumerable<Type>なので、ここでもゲートを作ってしまう。そして、AsParallelがハードコードされたことで、わざと非並列状態で実行させたくても出来ないという問題もある。
これらを解消するには、以下のようにすればよい。

public static class LinqExtensions
{
    // パブリックなデフォルトコンストラクタを持つ型を抽出する(非並列化)
    public static IEnumerable<Type> OfCreatable(this IEnumerable<Type> enumerable)
    {
        return
            from type in enumerable
            where
                (type.IsPublic == true) &&  // パブリックであり、
                (type.IsClass == true) &&   // クラスであり、
                (type.IsAbstract == false) &&   // 抽象クラスではなく
                (type.GetConstructor(Type.EmptyTypes) != null)  // パブリックなデフォルトコンストラクタがある
            select type;
    }

    // パブリックなデフォルトコンストラクタを持つ型を抽出する(並列化)
    public static ParallelQuery<Type> OfCreatable(this ParallelQuery<Type> parallelEnumerable)
    {
        return
            from type in parallelEnumerable
            where
                (type.IsPublic == true) &&  // パブリックであり、
                (type.IsClass == true) &&   // クラスであり、
                (type.IsAbstract == false) &&   // 抽象クラスではなく
                (type.GetConstructor(Type.EmptyTypes) != null)  // パブリックなデフォルトコンストラクタがある
            select type;
    }
}

要するに、引数(と戻り値)の列挙子の型が、ParallelQuery<T>となるようなオーバーロードを用意する。すると、中のLINQクエリの記述が全く同一であったとしても、それらの拡張メソッド(WhereやSelect等)は、Enumerable.WhereとParallelEnumerable.Whereのように呼び分けが行われる。結果として、並列バージョンではクエリ全体が並列化され、IEnumerable<T>のような通常の列挙子から呼び出される場合は、非並列クエリとなる。
これで目的を達したのだが、最後にこのメソッドを、非並列バージョンと並列バージョンのクラスに分ける。

// LINQ拡張メソッド群(非並列バージョン)
public static class LinqExtensions
{
    // パブリックなデフォルトコンストラクタを持つ型を抽出する
    public static IEnumerable<Type> OfCreatable(this IEnumerable<Type> enumerable)
    {
        return
            from type in enumerable
            where
                (type.IsPublic == true) &&  // パブリックであり、
                (type.IsClass == true) &&   // クラスであり、
                (type.IsAbstract == false) &&   // 抽象クラスではなく
                (type.GetConstructor(Type.EmptyTypes) != null)  // パブリックなデフォルトコンストラクタがある
            select type;
    }
}
// LINQ拡張メソッド群(並列バージョン)
public static class ParallelLinqExtensions
{
    // パブリックなデフォルトコンストラクタを持つ型を抽出する
    public static ParallelQuery<Type> OfCreatable(this ParallelQuery<Type> parallelEnumerable)
    {
        return
            from type in parallelEnumerable
            where
                (type.IsPublic == true) &&  // パブリックであり、
                (type.IsClass == true) &&   // クラスであり、
                (type.IsAbstract == false) &&   // 抽象クラスではなく
                (type.GetConstructor(Type.EmptyTypes) != null)  // パブリックなデフォルトコンストラクタがある
            select type;
    }
}

このように、非並列バージョンと並列バージョンの拡張メソッドを分けることで、丁度EnumerableクラスとParallelEnumerableクラスが分けられているのと同じ構造となる。