Advent LINQ (19) : Expressionのコンパイルと独立性

Expressionを取得するには、ラムダ式から生成するのが簡単だ。他にExpressionクラスに定義されている静的メソッドを使用して生成する事もできる。どちらにしても、LINQ to Objectsで使ってきたデリゲート(Func<T, U>)と異なり、Expressionを実行する事は出来ない。あくまでExpressionVisitorを使うなどして、式の構造を解析できるだけだ。しかし、ラムダ式に対応するLambdaExpressionは、「コンパイル」が可能だ。

// Expressionを生成
Expression<Func<bool>> func12 = () => "ABCDEFG".StartsWith("ABC") == true;
// Expressionをコンパイルし、デリゲートを生成
var func12Compiled = func12.Compile();
// 実行する
var result = func12Compiled();

「コンパイル」とは、実際にコンパイルされることだ。コンパイルされたデリゲートを使えば、任意の式を高速に実行出来る。便利そうであるが、実は複雑な問題が絡んでいる。Expressionを生成した際にあまり意識していないと思うが、Expressionのノードには「クロージャ」が含まれる場合がある。これは、ラムダ式を使うときには自然な概念として理解されていると思う。

public static Func<string> CreateClosure()
{
    // 暗黙のクロージャ(デリゲートを返す)
    var localVariable = 123;
    return () => localVariable.ToString();
}

localVariableはCreateClosureメソッドのローカル変数だ。常識的には、ローカル変数の内容は、メソッドを抜ける時に破棄される。上のコードではローカル変数を参照する「デリゲート」が、メソッドから返される。メソッドから返されたデリゲートは、果たして安全に実行できるだろうか?
C言語でローカル変数へのポインタを返してバグを招いたことは無いだろうか?しかし、上記のコードは問題なく動作する。localVariableが定数だから問題ないのだろうか?仮に定数ではなくても、このコードは正しく動作する。何故か?コンパイル時に生成される「クロージャクラス」のメンバフィールドが、localVariableに対応するように、コードが変更されてコンパイルされるからだ。
以下はこれを概念的に示したものだ。

// 不可視でコンパイラが自動的に生成するクロージャクラス
internal sealed class TemporaryClosureClass
{
    public int localVariable;
    public string Execute()
    {
        return localVariable.ToString();
    }
}
public static Func<string> CreateClosure()
{
    // 暗黙のクロージャ(デリゲートを返す)
    var closure = new TemporaryClosureClass();
    closure.localVariable = 123;
    return closure.Execute;
}

CreateClosureが返すデリゲートには、暗黙にTemporaryClosureClassクラスのインスタンスへの参照が含まれる。そのため、このインスタンスがGCに回収されることはなく、CreateClosureを抜けてからでも、問題なく実行する事が出来る。
では、デリゲートではなくExpressionならどうだろうか?

public static Expression<Func<string>> CreateClosure()
{
    // 暗黙のクロージャ(Expression)
    var closure = new TemporaryClosureClass();
    closure.localVariable = 123;
    return closure.Execute;     // <-- 構文エラー
}

残念ながら、「closure.Execute」はラムダ式ではないので、このコードはコンパイルして検証出来ない。しかし、式がMethodCallExpressionであるとして想像すると、次のようになるはずだ。

  • Method: TemporaryClosureClassクラスのExecuteメソッドを示す。
  • Arguments: Executeの引数(引数なしなので、空の配列)。
  • Object: closure変数への参照、つまりTemporaryClosureClassクラスのインスタンス。

このExpressionを前回のXML出力に掛けた場合、正しくXMLに変換できるだろうか?問題があるとすれば、Objectプロパティ、つまり、TemporaryClosureClassのインスタンスをどうやってXMLに変換するかだ。このようなクラスは、当然ToStringの明示的な実装を行っていない。そのため、残念ながら可読可能な状態での変換は望めないことになる。
そして、Expressionは暗黙的に、何らかのインスタンスに対しての参照を内包している可能性があることになる。そのため、Expressionを「コンパイル」出来たとしても、参照を内包している限りそれはその場限りでしか使えず、再利用して高速化するのは難しい。もし、再利用したい場合は、クロージャや外部インスタンスへの参照を暗黙に含まないように、注意してExpressionを組み立てる必要がある。

投稿者:

kekyo

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