Expression树的学习
表达式树是表示一些代码的数据结构。 它不是已编译且可执行的代码。LambdaExpression可以转换为可执行的代码。
如下一个简单表达式树:
var sum = 1 + 2;
该表达式被进一步细分为表示加法运算、该加法左操作数和右操作数的表达式。
整个语句是一棵树:应从根节点开始,浏览到树中的每个节点,以查看构成该语句的代码:
- 具有赋值 (
var sum = 1 + 2;
) 的变量声明语句- 隐式变量类型声明 (
var sum
)- 隐式 var 关键字 (
var
) - 变量名称声明 (
sum
)
- 隐式 var 关键字 (
- 赋值运算符 (
=
) - 二进制加法表达式 (
1 + 2
)- 左操作数 (
1
) - 加法运算符 (
+
) - 右操作数 (
2
)
- 左操作数 (
- 隐式变量类型声明 (
所支持的类型:
在语言设计中,表达式是可计算并返回值的代码主体。 表达式可能非常简单:常数表达式 1
返回常数值 1。 也可能较复杂:表达式 (-B + Math.Sqrt(B*B - 4 * A * C)) / (2 * A)
返回二次方程的一个根(若方程有解)。
Expression<Func<int, int>> addFive = (num) => num + 5; if (addFive.NodeType == ExpressionType.Lambda) { var lambdaExp = (LambdaExpression)addFive; var parameter = lambdaExp.Parameters.First(); Console.WriteLine(parameter.Name); Console.WriteLine(parameter.Type); }
上述代码打印表达式树addFive 的参数和类型。这样的addFive在实际上并没有用,不如直接使用委托Func。
创建一个“1+2”的表达式
// Addition is an add expression for "1 + 2" var one = Expression.Constant(1, typeof(int)); var two = Expression.Constant(2, typeof(int)); var addition = Expression.Add(one, two);
存在映射到 C# 语言的几乎所有语法元素的表达式节点类型。 每种类型都有针对该种语言元素的特定方法。 需要一次性记住的内容很多。 我不会记住所有内容,而是会采用有关使用表达式树的技巧,如下所示:
- 查看
ExpressionType
枚举的成员以确定应检查的可能节点。 如果想要遍历和理解表达式树,这将非常有用。 - 查看
Expression
类的静态成员以生成表达式。 这些方法可以从其子节点集生成任何表达式类型。 - 查看
ExpressionVisitor
类,以生成一个经过修改的表达式树。
注意事项
将 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
返回的函数之后,可随时访问该变量。
但是,请考虑实现 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
属性引用的代码时将出现 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
。
Expression<Func<int, int, int>> addition = (a, b) => a + b; Console.WriteLine($"This expression is a {addition.NodeType} expression type"); Console.WriteLine($"The name of the lambda is {((addition.Name == null) ? "<null>" : addition.Name)}"); Console.WriteLine($"The return type is {addition.ReturnType.ToString()}"); Console.WriteLine($"The expression has {addition.Parameters.Count} arguments. They are:"); foreach(var argumentExpression in addition.Parameters) { Console.WriteLine($"\tParameter Type: {argumentExpression.Type.ToString()}, Name: {argumentExpression.Name}"); } var additionBody = (BinaryExpression)addition.Body; Console.WriteLine($"The body is a {additionBody.NodeType} expression"); Console.WriteLine($"The left side is a {additionBody.Left.NodeType} expression"); var left = (ParameterExpression)additionBody.Left; Console.WriteLine($"\tParameter Type: {left.Type.ToString()}, Name: {left.Name}"); Console.WriteLine($"The right side is a {additionBody.Right.NodeType} expression"); var right= (ParameterExpression)additionBody.Right; Console.WriteLine($"\tParameter Type: {right.Type.ToString()}, Name: {right.Name}");
This expression is a/an Lambda expression type The name of the lambda is <null> The return type is System.Int32 The expression has 2 arguments. They are: Parameter Type: System.Int32, Name: a Parameter Type: System.Int32, Name: b The body is a/an Add expression The left side is a Parameter expression Parameter Type: System.Int32, Name: a The right side is a Parameter expression Parameter Type: System.Int32, Name: b