表达式树 - 定义代码的数据

表达式树是定义代码的数据结构。 表达式树基于编译器用来分析代码并生成已编译输出的相同结构。 阅读本文时,你会注意到表达式树与 Roslyn API 中用于生成 分析器和代码修复的类型之间存在相当的相似性。 (分析器和代码修补程序是 NuGet 包,用于对代码执行静态分析,并建议开发人员的潜在修补程序。这些概念相似,最终结果是一种数据结构,允许以有意义的方式检查源代码。 但是,表达式树基于与 Roslyn API 不同的类和 API 集。 下面是一行代码:

var sum = 1 + 2;

如果将上述代码分析为表达式树,则树包含多个节点。 最外层节点是具有赋值的变量声明语句(var sum = 1 + 2;) 最外部节点包含多个子节点:变量声明、赋值运算符和表示等号右侧的表达式。 该表达式进一步细分为表示加法运算的表达式,以及加法的左右作数。

让我们更深入地分析等号右侧的表达式。 表达式是 1 + 2二进制表达式。 更具体地说,它是二进制加法表达式。 二进制加法表达式有两个子级,表示加法表达式的左右节点。 在这里,这两个节点都是常量表达式:左侧作数是值 1,右作数是值 2

直观地说,整个语句是一个树:可以从根节点开始,并前往树中的每个节点以查看构成该语句的代码:

  • 具有赋值的变量声明语句 (var sum = 1 + 2;
    • 隐式变量类型声明 (var sum
      • 隐式 var 关键字 (var)
      • 变量名称声明 (sum
    • 赋值运算符 (=
    • 二进制加法表达式 (1 + 2
      • 左操作数 (1)
      • 加法运算符 (+
      • 右操作数 (2)

上面的树看起来可能很复杂,但它非常强大。 按照相同的过程,可以分解更复杂的表达式。 请考虑以下表达式:

var finalAnswer = this.SecretSauceFunction(
    currentState.createInterimResult(), currentState.createSecondValue(1, 2),
    decisionServer.considerFinalOptions("hello")) +
    MoreSecretSauce('A', DateTime.Now, true);

前面的表达式也是具有赋值的变量声明。 在此情况下,赋值的右侧是一棵更加复杂的树。 你不打算分解此表达式,但请思考一下不同的节点可能是什么。 使用当前对象作为接收方的方法调用有两种:一种具有显式 this 接收器,另一种没有显式接收器。 有方法调用使用其他接收方对象,有不同类型的常量参数。 最后,有一个二进制加法运算符。 该二进制加法运算符可能是对重写的加法运算符的方法调用(具体取决于 SecretSauceFunction()MoreSecretSauce() 的返回类型),解析为对为类定义的二进制加法运算符的静态方法调用。

尽管存在这种复杂性,但前面的表达式创建一个与第一个示例一样轻松导航的树结构。 保持遍历子节点,以查找表达式中的叶节点。 父节点具有对其子节点的引用,每个节点都有一个描述该节点类型的属性。

表达式树的结构非常一致。 了解基础知识后,当代码表示为表达式树时,你甚至可以理解最复杂的代码。 数据结构中的优雅性说明了 C# 编译器如何分析最复杂的 C# 程序,并从该复杂的源代码创建正确的输出。

熟悉表达式树的结构后,你会发现你已快速获得的知识使你能够处理更多更高级的方案。 表达式树的功能非常强大。

除了转换算法以在其他环境中执行外,表达式树还能够更轻松地编写在执行代码之前检查代码的算法。 编写一个方法,其参数是表达式,然后在执行代码之前检查这些表达式。 表达式树是代码的完整表示形式:可以看到任何子表达式的值。 你将看到方法和属性名称。 可以看到任何常量表达式的值。 您将表达式树转换为可执行委托,然后执行该代码。

表达式树的 API 使你能够创建表示几乎任何有效代码构造的树。 但是,为了尽可能保持简单,有些 C# 惯用法无法在表达式树中生成。 一个示例是异步表达式(使用 asyncawait 关键字)。 如果您的需求需要异步算法,则需要直接操作 Task 对象,而不是依靠编译器支持。 另一个示例是创建循环。 通常,使用forforeachwhiledo循环创建这些循环。 如本系列后面所示,表达式树的 API 支持单循环表达式,以及breakcontinue用于控制重复循环的表达式。

你不能做的一件事情是修改表达式树。 表达式树是不可变的数据结构。 如果您想要改变(更改)表达式树,您必须创建一棵新树,该树是原始树的副本,但包含您所需的更改。