表达式树说明
表达式树是定义代码的数据结构。 它们基于编译器用于分析代码和生成已编译输出的相同结构。 读完本教程后,你会注意到表达式树和 Roslyn API 中用于生成分析器和 CodeFixes 的类型之间存在很多相似之处。 (分析器和 CodeFix 是对代码执行静态分析的 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
)
- 隐式 var 关键字 (
- 赋值运算符 (
=
) - 二进制加法表达式 (
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# 习惯用语。 其中一个示例就是异步表达式(使用 async
和 await
关键字)。 如果需要异步算法,则需要直接操作 Task
对象,而不是依赖于编译器支持。 另一个示例是创建循环。 通常,通过使用 for
、foreach
、while
或 do
循环对其进行创建。 正如稍后可以在本系列中看到的那样,表达式树的 API 支持单个循环表达式,该表达式包含控制重复循环的 break
和 continue
表达式。
不能执行的操作是修改表达式树。 表达式树是不可变的数据结构。 如果想要改变(更改)表达式树,则必须创建基于原始树副本但包含所需更改的新树。