式ツリーは、いくつかのコードを表すデータ構造です。 コンパイルおよび実行可能コードではありません。 式ツリーで表される .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.CompileメンバーとLambdaExpression.CompileToMethodメンバーが含まれています。
Compile
メソッドはデリゲートを作成します。
CompileToMethod
メソッドは、式ツリーのコンパイル済み出力を表す IL を使用して、System.Reflection.Emit.MethodBuilder オブジェクトを更新します。
Von Bedeutung
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
を実装する変数にアクセスする場合、オブジェクトが式ツリーによって保持されている間にコードで破棄される可能性があります。
たとえば、 int
は IDisposable
を実装していないため、このコードは正常に動作します。
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;
}
}
次のコードに示すように式で使用すると、System.ObjectDisposedException プロパティによって参照されるコードを実行するときにResource.Argument
が表示されます。
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 つのメカニズムを提供します。
式ツリーは、作成した特定のコンストラクトに対して実行されるコードを表します。 コードをコンパイルして実行する環境が式を作成する環境と一致する限り、すべてが期待どおりに動作します。 それが発生しない場合、エラーは予測可能であり、式ツリーを使用するコードの最初のテストでキャッチされます。
.NET