表达式树是表示某些代码的数据结构。 它不是编译后的可执行代码。 如果要执行表达式树所表示的 .NET 代码,则必须将其转换为可执行的 IL 指令。 执行表达式树可能会返回值,也可以只执行调用方法等作。
仅可以执行表示 lambda 表达式的表达式树。 表示 lambda 表达式的表达式树的类型 LambdaExpression 或 Expression<TDelegate>。 若要执行这些表达式树,请调用 Compile 方法以创建可执行委托,然后调用委托。
注释
如果委托的类型未知,也就是说,lambda 表达式的类型为 LambdaExpression 而非 Expression<TDelegate>,请在委托上调用 DynamicInvoke 方法,而不是直接调用委托。
如果表达式树不表示 lambda 表达式,则可以通过调用 Lambda<TDelegate>(Expression, IEnumerable<ParameterExpression>) 方法创建一个新的 lambda 表达式,该表达式树具有原始表达式树作为其主体。 然后,可以按照本节前面所述执行 lambda 表达式。
将 Lambda 表达式转换为函数
可以将任何 LambdaExpression 或派生自 LambdaExpression 的任何类型转换为可执行 IL。 其他表达式类型无法直接转换为代码。 实际上,这种限制没有什么影响。 Lambda 表达式是你想要通过转换为可执行中间语言(IL)来执行的唯一类型的表达式。 (思考直接执行 System.Linq.Expressions.ConstantExpression 意味着什么。这是否意味着任何用处?)System.Linq.Expressions.LambdaExpression 或派生自 LambdaExpression
的类型的任何表达式树均可转换为 IL。 表达式类型 System.Linq.Expressions.Expression<TDelegate> 是 .NET Core 库中的唯一具体示例。 它用于表示映射到任何委托类型的表达式。 由于此类型映射到委托类型,因此 .NET 可以检查表达式,并为与 lambda 表达式签名匹配的相应委托生成 IL。 代理类型基于表达式类型。 如果要以强类型化的方式使用委托对象,则必须知道返回类型和参数列表。 该方法 LambdaExpression.Compile()
返回类型 Delegate
。 你必须将其转换为正确的委托类型,以便任何编译时工具能够检查参数列表或返回类型。
在大多数情况下,表达式和其对应的委托之间存在简单映射。 例如,由 Expression<Func<int>>
表示的表达式树将被转换为 Func<int>
类型的委托。 对于具有任何返回类型和参数列表的 lambda 表达式,存在一个委托类型,该委托类型是该 lambda 表达式所表示的可执行代码的目标类型。
该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.
下面的代码示例演示如何执行表达式树,该树表示通过创建 lambda 表达式并执行该表达式将数字提升到幂。 示例最后显示幂运算的结果。
// 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()
来调用该委托,从而执行代码。
该委托表示表达式树中的代码。 可以保留该委托的句柄并在稍后调用它。 每次要执行表达式树所表示的代码时,无需编译表达式树。 (请记住,表达式树是不可变的,稍后编译相同的表达式树将创建执行相同代码的委托。
谨慎
不要创建任何更复杂的缓存机制,以避免不必要的编译调用来提高性能。 比较两个任意表达式树以确定它们是否表示同一算法是一个耗时的作。 通过避免对 LambdaExpression.Compile()
的任何额外调用所节省的计算时间可能多于执行代码(该代码用于确定两个不同的表达式树是否可产生相同的可执行代码)所花费的时间。
注意事项
将 lambda 表达式编译为委托并调用该委托是你可以通过表达式树完成的最简单操作之一。 但是,即使如此简单的操作,也存在一些需要注意的事项。
Lambda 表达式会在表达式中引用的任何局部变量上创建闭包。 必须确保任何可能会成为委托一部分的变量都在调用 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
。
概要
可以编译表示 lambda 表达式的表达式树,以创建可执行的委托。 表达式树提供一种机制来执行由表达式树表示的代码。
表达式树表示将针对所创建的任何给定构造执行的代码。 只要编译和执行代码的环境与创建表达式的环境匹配,所有内容都按预期工作。 如果未按预期进行,那么错误是可以预知的,并且将在使用表达式树的任何代码的第一批测试中捕获这些错误。