表达树

表达树简介

我们来自哪里

表达式树都是关于在运行时消耗源代码。考虑一种计算销售订单 decimal CalculateTotalTaxDue(SalesOrder order) 的销售税的方法。在 .NET 程序中使用该方法很简单 - 你只需将其称为 decimal taxDue = CalculateTotalTaxDue(order);。如果要将其应用于远程查询(SQL,XML,远程服务器等)的所有结果,该怎么办?那些远程查询源无法调用该方法! 传统上,在所有这些情况下,你都必须反转流量。创建整个查询,将其存储在内存中,然后遍历结果并计算每个结果的税。

如何避免流量反转的内存和延迟问题

表达式树是树格式的数据结构,其中每个节点都包含一个表达式。它们用于转换表达式中的编译指令(如用于过滤数据的方法),这些指令可以在程序环境之外使用,例如在数据库查询中。

这里的问题是远程查询无法访问我们的方法。我们可以避免这个问题,相反,我们将方法的指令发送到远程查询。在我们的 CalculateTotalTaxDue 示例中,这意味着我们发送此信息:

  1. 创建一个变量来存储总税额
  2. 循环遍历订单上的所有行
  3. 对于每一行,检查产品是否应纳税
  4. 如果是,则将总线乘以适用的税率,并将该金额添加到总额中
  5. 否则什么也不做

使用这些指令,远程查询可以在创建数据时执行工作。

实现这一点有两个挑战。如何将已编译的 .NET 方法转换为指令列表,以及如何以远程系统可以使用的方式格式化指令?

没有表达式树,你只能解决 MSIL 的第一个问题。 (MSIL 是由 .NET 编译器创建的类似汇编程序的代码。)解析 MSIL 是可能的,但这并不容易。即使你正确解析它,也很难确定原始程序员对特定例程的意图。

表达树节省了一天

表达式树解决了这些确切问题。它们表示程序指令树数据结构,其中每个节点代表一条指令,并且引用了执行该指令所需的所有信息。例如,MethodCallExpression 参考 1)它要调用的 MethodInfo,2)它将传递给该方法的 Expressions 列表,3)例如方法,你将调用方法的 Expression。你可以走树并应用远程查询的说明。

创建表达式树

创建表达式树的最简单方法是使用 lambda 表达式。这些表达式看起来与普通的 C#方法几乎相同。重要的是要意识到这是编译器的魔力。首次创建 lambda 表达式时,编译器会检查你为其分配的内容。如果它是 Delegate 类型(包括 ActionFunc),编译器会将 lambda 表达式转换为委托。如果它是 LambdaExpression(或 Expression<Action<T>>Expression<Func<T>>,它们是强类型 LambdaExpression 的),编译器会将其转换为 LambdaExpression。这就是魔术的结果。在幕后,编译器使用表达式树 API 将你的 lambda 表达式转换为 LambdaExpression

Lambda 表达式无法创建每种类型的表达式树。在这些情况下,你可以手动使用 Expressions API 来创建所需的树。在 Understanding the expressions API 示例中,我们使用 API​​创建 CalculateTotalSalesTax 表达式。

注意:名称在这里有点令人困惑。甲 lambda 表达式 (两个字,小写)指的代码与 => 指示符的块。它代表 C#中的匿名方法,并转换为 DelegateExpression。甲 LambdaExpression (一个字,PascalCase)指的是表达 API 代表可以执行的方法中的节点类型。

表达式树和 LINQ

表达式树的最常见用途之一是使用 LINQ 和数据库查询。LINQ 将表达式树与查询提供程序配对,以将指令应用于目标远程查询。例如,LINQ to Entity Framework 查询提供程序将表达式树转换为 SQL,该 SQL 直接针对数据库执行。

将所有部分组合在一起,你可以看到 LINQ 背后的真正力量。

  1. 使用 lambda 表达式编写查询:products.Where(x => x.Cost > 5)
  2. 编译器将该表达式转换为表达式树,其中包含“检查参数的 Cost 属性是否大于 5”的指令。
  3. 查询提供程序解析表达式树并生成有效的 SQL 查询 SELECT * FROM products WHERE Cost > 5
  4. ORM 将所有结果投射到 POCO 中,然后返回一个对象列表

笔记

  • 表达式树是不可变的。如果要更改表达式树,则需要创建一个新表达式树,将现有树复制到新表达树中(遍历表达式树,你可以使用 ExpressionVisitor)并进行所需的更改。