式ツリーを実行する

式ツリーはコードを表すデータ構造です。 コンパイル済みの実行可能なコードではありません。 式ツリーで表される .NET コードを実行する場合は、実行可能な IL 命令に変換する必要があります。 式ツリーを実行すると値が返される場合がありますが、メソッドの呼び出しなどの処理が実行されるだけの場合もあります。

実行できるのは、ラムダ式を表す式ツリーのみです。 ラムダ式を表す式ツリーの型は、LambdaExpression または Expression<TDelegate> です。 このような式ツリーを実行するには、Compile メソッドを呼び出して実行可能なデリゲートを作成した後、そのデリゲートを呼び出します。

注意

デリゲートの型が不明な場合、つまり、ラムダ式が LambdaExpression 型であり Expression<TDelegate> 型ではない場合は、DynamicInvoke メソッドを、直接呼び出すのではなく、デリゲートに対して呼び出します。

式ツリーがラムダ式を表さない場合は、Lambda<TDelegate>(Expression, IEnumerable<ParameterExpression>) メソッドを呼び出すことで、元の式ツリーを本体に含む新しいラムダ式を作成できます。 その後、このセクションの説明のとおりにラムダ式を実行できます。

ラムダ式から関数

すべての LambdaExpression、または LambdaExpression の派生型は、実行可能な IL に変換できます。 その他の式の型は、直接コードに変換できません。 実際には、この制限はほとんど影響がありません。 ラムダ式は、実行可能な中間言語 (IL) に変換して実行する唯一の式の型です。 (直接 System.Linq.Expressions.ConstantExpression を実行することにどのような意味があるでしょうか。何かに役立つでしょうか。) System.Linq.Expressions.LambdaExpression である式ツリー、または LambdaExpression の派生型はすべて IL に変換できます。 式の型 System.Linq.Expressions.Expression<TDelegate> は .NET Core ライブラリで唯一の具体的な例です。 この型は任意のデリゲート型にマップされる式を表すのに使用されます。 この型はデリゲート型にマップされるため、.NET で式を検証し、ラムダ式のシグネチャと一致する適切なデリゲートの IL を生成することができます。 デリゲート型は式の型が基になっています。 厳密に型指定された方法でデリゲート オブジェクトを使用する場合は、戻り値の型および引数リストを把握する必要があります。 LambdaExpression.Compile() メソッドは Delegate 型を返します。 コンパイル時ツールを使って、引数リストまたは戻り値の型をチェックするには、適切なデリゲート型にキャストする必要があります。

通常、式とそれに対応するデリゲートの間には単純なマッピングが存在します。 たとえば、Expression<Func<int>> によって表される式ツリーは、Func<int> 型のデリゲートに変換されます。 任意の戻り値の型と引数リストをもつラムダ式の場合、そのラムダ式によって表される実行可能コードのターゲット型となるデリゲート型が存在します。

System.Linq.Expressions.LambdaExpression 型には、式ツリーを実行可能コードに変換するために使用する LambdaExpression.CompileLambdaExpression.CompileToMethod メンバーが含まれます。 Compile メソッドはデリゲートを作成します。 CompileToMethod メソッドは、式ツリーのコンパイル済み出力を表す IL によって System.Reflection.Emit.MethodBuilder オブジェクトを更新します。

重要

CompileToMethod は .NET Framework でのみ使用でき、.NET Core または .NET 5 以降では使用できません。

必要に応じて、生成されたデリゲート オブジェクトのシンボル デバッグ情報を受け取る System.Runtime.CompilerServices.DebugInfoGenerator を提供することもできます。 DebugInfoGenerator では、生成されたデリゲートに関する完全なデバッグ情報が提供されます。

次のコードを使用して、式をデリゲートに変換します。

Expression<Func<int>> add = () => 1 + 2;
var func = add.Compile(); // Create Delegate
var answer = func(); // Invoke Delegate
Console.WriteLine(answer);

次のコード例では、式ツリーをコンパイルして実行するときに使われる具象型を示します。

Expression<Func<int, bool>> expr = num => num < 5;

// Compiling the expression tree into a delegate.
Func<int, bool> result = expr.Compile();

// Invoking the delegate and writing the result to the console.
Console.WriteLine(result(4));

// Prints True.

// You can also use simplified syntax
// to compile and run an expression tree.
// The following line can replace two previous statements.
Console.WriteLine(expr.Compile()(4));

// Also prints True.

次のコード例は、ラムダ式を作成して実行することで数値の累乗を表す式ツリーの実行方法を示しています。 このコードを実行すると、累乗された数値を表す結果が表示されます。

// The expression tree to execute.
BinaryExpression be = Expression.Power(Expression.Constant(2d), Expression.Constant(3d));

// Create a lambda expression.
Expression<Func<double>> le = Expression.Lambda<Func<double>>(be);

// Compile the lambda expression.
Func<double> compiledExpression = le.Compile();

// Execute the lambda expression.
double result = compiledExpression();

// Display the result.
Console.WriteLine(result);

// This code produces the following output:
// 8

実行と有効期間

LambdaExpression.Compile() を呼び出したときに作成されたデリゲートを呼び出して、コードを実行します。 上のコードの add.Compile() はデリゲートを返します。 func() を呼び出してそのデリゲートを呼び出すと、コードが実行されます。

そのデリゲートは式ツリーのコードを表します。 そのデリゲートのハンドルを保持して、後で呼び出すことができます。 デリゲートが表すコードを実行するたびに、式ツリーをコンパイルする必要はありません。 (式ツリーは不変であるため、後で同じ式ツリーをコンパイルすると、同じコードを実行するデリゲートが作成されることに注意してください。)

注意

不要なコンパイルの呼び出しを回避してパフォーマンスを向上させるために、これ以上高度なキャッシュ メカニズムを作成しないでください。 2 つの任意の式ツリーを比較して、同じアルゴリズムを表しているかどうかを判定するのは、時間のかかる操作です。 LambdaExpression.Compile() への余分な呼び出しを回避して、コンピューティング時間を削減しても、2 つの異なる式ツリーの結果が同じ実行可能コードになるかどうかを判断するコードの実行に、さらに多くの時間がかかる可能性があります。

注意事項

ラムダ式をデリゲートにコンパイルして、そのデリゲートを呼び出すのは、式ツリーで実行する最も単純な操作の 1 つです。 ただし、この簡単な操作にも気をつけるべき点があります。

ラムダ式は、式で参照される任意のローカル変数に対するクロージャを作成します。 デリゲートの一部となる任意の変数は、Compile を呼び出す場所で使用でき、得られたデリゲートを実行するときに使用できるように保証する必要があります。 コンパイラにより、変数がスコープ内であることが保証されます。 ただし、IDisposable を実装する変数に式がアクセスする場合は、オブジェクトが式ツリーによってまだ保持されている間に、コードがオブジェクトを破棄する可能性があります。

たとえば、次のコードは、intIDisposable が実装されていないため、問題なく機能します。

private static Func<int, int> CreateBoundFunc()
{
    var constant = 5; // constant is captured by the expression tree
    Expression<Func<int, int>> expression = (b) => constant + b;
    var rVal = expression.Compile();
    return rVal;
}

デリゲートはローカル変数 constant への参照を取得します。 CreateBoundFunc によって返される関数をあとで実行するとき、その変数はいつでもアクセスできます。

一方、System.IDisposable を実装する次のような (かなり不自然な) クラスはどうでしょうか。

public class Resource : IDisposable
{
    private bool _isDisposed = false;
    public int Argument
    {
        get
        {
            if (!_isDisposed)
                return 5;
            else throw new ObjectDisposedException("Resource");
        }
    }

    public void Dispose()
    {
        _isDisposed = true;
    }
}

次のコードで示されている式でそれを使う場合、Resource.Argument プロパティによって参照されるコードを実行すると、System.ObjectDisposedException が得られます。

private static Func<int, int> CreateBoundResource()
{
    using (var constant = new Resource()) // constant is captured by the expression tree
    {
        Expression<Func<int, int>> expression = (b) => constant.Argument + b;
        var rVal = expression.Compile();
        return rVal;
    }
}

このメソッドから返されたデリゲートは、constant オブジェクトを捕捉しますが、このオブジェクトはすでに破棄されています。 (オブジェクトは using ステートメントで宣言されたため、破棄されています。)

このため、このメソッドから返されたデリゲートを実行すると、実行時に ObjectDisposedException がスローされます。

コンパイル時のコンストラクトを表す実行時エラーが発生するのは変だと思うかもしれませんが、式ツリーを扱う場合はそういうものです。

この問題には多くのバリエーションがあるため、これを回避する一般的なガイダンスを提供することは困難です。 式を定義するときはローカル変数へのアクセスについて注意し、パブリック API によって返される式ツリーを作成するときは、(this によって表される) 現在のオブジェクトでのアクセス状態に注意してください。

式のコードが他のアセンブリのメソッドまたはプロパティを参照する場合があります。 そのアセンブリには、式の定義時、式のコンパイル時、結果のデリゲートの呼び出し時に、アクセスできる必要があります。 それが存在しない場合、ReferencedAssemblyNotFoundException が発生します。

まとめ

ラムダ式を表す式ツリーをコンパイルすると、実行可能なデリゲートを作成できます。 式ツリーでは、式ツリーが表すコードを実行するための 1 つのメカニズムが用意されています。

式ツリーは、作成した任意の構成要素を実行するコードを表わしています。 コードをコンパイルして実行する環境が、式を作成する環境と一致している限り、すべてが期待どおりに動作します。 そうでない場合は、エラーが予想され、式ツリーを使用するコードの最初のテストで検出されます。