https://learn.microsoft.com/zh-cn/dotnet/api/system.linq.expressions?view=net-9.0
https://learn.microsoft.com/zh-cn/dotnet/csharp/advanced-topics/expression-trees/debugging-expression-trees-in-visual-studio
表达式树是什么
- 表达式:是用于 计算 并 返回值 的代码片段。表达式通常有明确的返回值,可以是算术运算、方法调用、常量值等。
- 代码 是编程语言中用于定义程序行为的指令集合。在 C# 中,代码通常指 源代码,它是由 语句 和 表达式 组成的。
- 语句 是程序中执行某个操作的单元,它通常不直接返回值,但它可能会改变程序的状态或执行某个操作(如调用方法、循环、条件判断等)。语句不一定有返回值。
- 数据结构 存储和组织数据的方式。通过特定方式组织数据,便于访问和操作。
- 表达式树:是将 表达式 转换为数据结构(树形结构)的方式。它不仅仅是表示程序中的计算表达式,还为程序分析、优化和动态执行提供了灵活的表示。表达式树是 C# 中的一个类,它能够表示和操作表达式的结构,可以在运行时动态构建、修改、执行或者优化表达式。
[!note] 微软官方给的定义
表达式树是定义代码的数据结构。 表达式树基于编译器用于分析代码和生成已编译输出的相同结构。
表达式树将代码中的表达式(例如数学运算、方法调用、变量赋值等)转化为一种树形结构的 数据模型。每个 节点 表示代码中的一个操作(如加法、除法、变量等),而树的结构则反映了代码执行的顺序或逻辑结构。
表达式树是由 System.Linq.Expressions 命名空间提供的一组类,表示代码中的表达式结构。通过表达式树,你可以:
- 动态创建表达式:在运行时生成代码逻辑。
- 操作现有表达式:修改或优化表达式。
- 编译表达式:将表达式树转换为可执行代码。
例如,一个数学表达式 a + b * c 会被分解为一棵树,其中根节点是 +,左节点是 a,右节点是 b * c,而 b * c 又是一个子树。
1 | + |
表达式树类型
表达式数据核心类
| Expression子类 | 描述 | 示例 |
|---|---|---|
ConstantExpression |
表示一个常量值(如数字、字符串等)。用于表达常量。 | Expression.Constant(5):表示常量值 5。 |
ParameterExpression |
表示一个方法参数,通常用于 Lambda 表达式或函数的输入参数。 | Expression.Parameter(typeof(int), "x"):表示一个名为 x 的 int 类型的参数。 |
BinaryExpression |
表示二元操作符的表达式(如加法、减法、乘法、除法等)。用于构建两个操作数之间的运算。 | Expression.Add(x, y):表示 x + y。var assignExpr = Expression.Assign(x, Expression.Constant(5)); // 表示 x = 5 |
UnaryExpression |
表示一元操作符的表达式(如取反、取负等)。用于构建一元操作符的运算。 | Expression.Negate(x):表示 -x。 |
ConditionalExpression |
表示条件表达式,类似于 if-else 或三元运算符 x > 10 ? x : 0。用于条件判断。 |
Expression.Condition(Expression.GreaterThan(x, Expression.Constant(10)), x, Expression.Constant(0)):表示 x > 10 ? x : 0。 |
NewExpression |
表示对象或结构体的实例化(调用构造函数)。用于创建对象 | Expression.New(typeof(Person).GetConstructor(new Type[] { typeof(string), typeof(int) }), Expression.Constant("John"), Expression.Constant(30)):表示 new Person("John", 30)。 |
MethodCallExpression |
表示方法调用的表达式,用于表示对方法的调用。 | Expression.Call(typeof(Math).GetMethod("Abs", new Type[] { typeof(int) }), x):表示 Math.Abs(x)。 |
MemberExpression |
表示对字段、属性或数组元素的访问。用于访问对象成员(字段或属性)。 | Expression.Property(person, "Name"):表示 person.Name。 |
IndexExpression |
表示对数组或集合的索引访问。用于表示对数组或集合元素的访问。 | Expression.ArrayIndex(array, index):表示 array[index]。 |
TypeBinaryExpression |
表示类型检查操作(is 或 as)。用于测试对象是否是某个特定类型的实例。 |
Expression.TypeIs(x, typeof(string)):表示 x is string。 |
BlockExpression |
表示一个由多个表达式组成的块语句(类似于一个代码块)。可以包含多个语句并且有一个返回值。 | Expression.Block(typeof(void), Expression.Call(...), Expression.Add(x, y)):表示多个语句构成的代码块。 |
CatchBlockExpression |
表示异常捕获块,通常与 TryExpression 一起使用,表示捕获某种异常并执行特定操作。 |
Expression.Catch(typeof(ArgumentException), Expression.Constant("Caught exception")):表示捕获 ArgumentException 并返回 "Caught exception"。 |
TryExpression |
表示 try-catch-finally 语句,用于表示捕获异常的逻辑。 |
Expression.TryCatch(Expression.Call(...), Expression.Catch(typeof(ArgumentException), Expression.Constant("Error caught"))):表示 try 块,捕获 ArgumentException 并处理。 |
GotoExpression |
表示跳转(goto)语句,通常与 LabelTarget 配合使用,表示控制流的跳转。 |
Expression.Goto(label):表示 goto label。 |
LabelExpression |
表示一个标签,通常与 GotoExpression 一起使用,定义跳转目标。 |
Expression.Label(label, Expression.Constant(42)):表示标签 label,并返回值 42。 |
LoopExpression |
表示一个循环(for, while, do-while)语句,通常与 LabelTarget 一起使用,表示控制流的循环。 |
Expression.Loop(Expression.Label(label), Expression.Constant(42)):表示循环,直到跳转到标签 label。 |
LambdaExpression |
表示一个 Lambda 表达式,包含参数和一个表达式体,用于表示匿名函数。通常是表达式树的最终构建目标,能被编译成委托。 | Expression.Lambda(Expression.Add(x, y), x, y):表示 Lambda 表达式 (x, y) => x + y。 |
拆分表达式和组装表达式树
拆分和组装一个正常的表达式成表达式树(Expression Tree)是一个涉及到递归和对表达式的解析的过程。这个过程的目的是将普通的数学表达式或逻辑表达式转换成一个结构化的、可以操作和编译的表达式树,通常这在动态构建查询、构造动态代码或实现复杂逻辑时非常有用。
- 优先级和括号处理:始终从内向外、从左到右解析表达式,并根据操作符优先级处理。
- 递归解析:对于复杂的表达式,使用递归来拆解每个操作符及其左右子表达式。
- 条件表达式和逻辑表达式:表达式树不仅限于数学运算,还可以表示条件、方法调用等各种复杂操作
标准的表达式( x + y * z)拆解成表达式树
- 解析字符串表达式:首先,我们需要解析输入的字符串表达式,将其拆解为操作数和操作符(如
x、+、y、*、z)。 - 转换成
Expression节点:一旦我们得到操作数和操作符,接下来将它们转换为相应的Expression类型节点(如ParameterExpression、ConstantExpression、BinaryExpression)。 - 构建表达式树:根据操作符优先级和括号,按照从左到右的顺序构建一个二叉表达式树。
x + y * z具体的过程:输出结果1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19static void Main(string[] args)
{
ParameterExpression x = Expression.Parameter(typeof(int), "x");
ParameterExpression y = Expression.Parameter(typeof(int), "y");
ParameterExpression z = Expression.Parameter(typeof(int), "z");
// 1. 先处理乘法部分: y * z
BinaryExpression multiplyExpr = Expression.Multiply(y, z);
// 2. 然后处理加法部分: x + (y * z)
BinaryExpression addExpr = Expression.Add(x, multiplyExpr);
// 3. 输出结果
Console.WriteLine("Expression Tree: " + addExpr);
// 4. 创建 Lambda 表达式
LambdaExpression lambda = Expression.Lambda(addExpr, x, y, z);
Console.WriteLine("Lambda Expression: " + lambda);
// 5. 编译并执行 Lambda 表达式
var compiled = (Func<int, int, int, int>)lambda.Compile();
int result = compiled(1, 2, 3); // x=1, y=2, z=3
Console.WriteLine("Result: " + result); // 输出 7
}1
2
3Expression Tree: (x + (y * z))
Lambda Expression: x => (x + (y * z))
Result: 7
条件表达式x > 10 ? x : y拆解成表达式树
1 | static void Main(string[] args) |
包含方法调用的代码段
通用的计算类:
1 | public class Calculator |
示例1
1 | (int x, int y) => |
这个代码段包括了:
- 输入参数:
x和y - 局部变量定义:
s = x + yl = x - y
- 方法调用:
m = calculate.Calculate(s, l) - 返回值:
return m
构建表达式树:
- 创建参数表达式:代表
x和y。 - 创建局部变量表达式:代表
s和l。 - 方法调用的表达式:调用
calculate.Calculate(s, l)。 - 构建返回值表达式:返回方法调用的结果。
代码实现:结果输出:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57using System;
using System.Linq.Expressions;
using System.Reflection;
class Program
{
static void Main(string[] args)
{
// 创建参数表达式 x 和 y
ParameterExpression x = Expression.Parameter(typeof(int), "x");
ParameterExpression y = Expression.Parameter(typeof(int), "y");
// 创建 Calculator 类型的表达式
Type calculatorType = typeof(Calculator);
// 获取 Calculate 方法的 MethodInfo
MethodInfo methodInfo = calculatorType.GetMethod("Calculate");
// 1. 创建 s = x + y 的表达式
BinaryExpression sExpr = Expression.Add(x, y);
// 2. 创建 l = x - y 的表达式
BinaryExpression lExpr = Expression.Subtract(x, y);
// 3. 创建方法调用表达式: calculate.Calculate(s, l)
MethodCallExpression callExpr = Expression.Call(
Expression.New(calculatorType), // 实例化 Calculator 对象
methodInfo, // 方法信息
sExpr, lExpr // 方法参数 s 和 l
);
Console.WriteLine("-----------------------------------------方式1---------------------------------------");
//方式1: (int x, int y) => new Calculator().Calculate(x + y, x - y) 简化源代码
Expression<Func<int, int, int>> lambdaExpr1 = Expression.Lambda<Func<int, int, int>>(callExpr, x, y);
Console.WriteLine(lambdaExpr1.ToString("C#"));
var fun1 = lambdaExpr1.Compile();
Console.WriteLine(fun1(5, 8));
Console.WriteLine("-----------------------------------------方式2---------------------------------------");
//创建result变量
ParameterExpression resultExpr = Expression.Parameter(typeof(int), "result");
// ParameterExpression a = Expression.Parameter(typeof(int), "a");
//赋值 int result= Calculate(x + y, x - y)
BinaryExpression assignExpr = Expression.Assign(resultExpr, callExpr);
//组装代码段
var blockExpr = Expression.Block(
[],
assignExpr,
resultExpr /*最后一个是返回的结果,没有返回值,可用 Expression.Empty()*/
);
//lambda
Expression<Func<int, int, int>> lambdaExpr2 = Expression.Lambda<Func<int, int, int>>(blockExpr, x, y);
Console.WriteLine(lambdaExpr2.ToString("C#"));
var fun2 = lambdaExpr2.Compile();
Console.WriteLine(fun2(5, 8));
}
}1
2
3
4
5
6
7
8
9
10-----------------------------------------方式1---------------------------------------
(int x, int y) => new Calculator().Calculate(x + y, x - y)
10
-----------------------------------------方式2---------------------------------------
(int x, int y) => {
int result;
result = new Calculator().Calculate(x + y, x - y);
return result;
}
10
示例2
1 | Func<int, int, int> fun1 = (x, y) => |
代码实例:
1 | static void Main(string[] args) |
输出:
1 | (x, y) => IIF((new [] {1233, 333, 821, 324, 4, 111, 892, 52, 81, 19}[(x % 10)] > new [] {1233, 333, 821, 324, 4, 111, 892, 52, 81, 19}[(y % 10)]), new Calculator().Calculate(new [] {1233, 333, 821, 324, 4, 111, 892, 52, 81, 19}[(x % 10)], new [] {1233, 333, 821, 324, 4, 111, 892, 52, 81, 19}[(y % 10)]), new Calculator().Calculate(new [] {1233, 333, 821, 324, 4, 111, 892, 52, 81, 19}[(y % 10)], new [] {1233, 333, 821, 324, 4, 111, 892, 52, 81, 19}[(x % 10)])) |
用代码块实现方式:
1 | static void Main(string[] args) |
输出结果
1 | sss:{var array;var a1;var b1;var calculator;var result; ... } |
通过频繁调用编译后的表达式,方式2实现执行效率高于方式1实现
示例3
1 | Func<int, int, int> fun = (x, y) => |
代码实现
1 | static void Main(string[] args) |
输出结果
1 | ( |
Lambda 表达式(LambdaExpression)
LambdaExpression 是 Expression 的一个特殊子类,它代表一个完整的函数(Lambda 表达式)。它将所有这些构件(如常量、变量、运算符等)组合成一个可以编译并执行的代码块。
表达式树的最终目标通常是动态生成一个 Lambda 表达式并将其编译为可执行代码。这是表达式树设计的一个重要用途——它允许在运行时构建和执行动态代码,而不需要预先编写和编译源代码。通过 LambdaExpression,你可以将一系列 Expression 节点组合成一个完整的函数,并通过 Compile() 方法将其编译成一个委托,从而执行该函数。
表达式树的应用场景
- 动态代码生成:动态构建代码并编译执行。
- 查询生成与翻译:例如,EF Core 使用表达式树来生成 SQL 查询。
- 代码行为修改:例如,动态代理或 AOP 中使用表达式树来修改方法行为。
- 延迟执行和缓存:构建表达式树时,延迟执行直到需要时才执行。
- 代码分析与元编程:动态解析和分析现有代码或表达式。 Roslyn 提供了强大的 代码分析与元编程 功能,通过表达式树和语法树动态解析和修改 C# 代码。
- 动态条件和选择:根据条件动态选择不同的执行路径。
- 优化和重构表达式树:减少冗余操作,提高效率。
1. Entity Framework (EF Core)
Entity Framework 和 EF Core 使用表达式树来动态生成 SQL 查询。EF 使用表达式树来将 LINQ 查询转化为数据库查询。
1 | var query = dbContext.Users |
在 EF Core 中,当你运行 LINQ 查询时,查询表达式被解析为一个 Expression 对象,EF Core 使用这个表达式对象构建相应的 SQL 查询。EF Core 会遍历表达式树中的各个节点,并根据节点类型生成 SQL 查询。
EF Core 并不会直接将查询表达式树转化为 Lambda 并执行,而是通过解析和分析表达式树来生成 SQL 查询并发送到数据库。表达式树在 EF Core 中扮演着非常重要的角色,它用于表示查询逻辑,并被用于生成数据库查询的 SQL。表达式树在 LINQ 查询解析、翻译、优化等多个阶段都发挥着关键作用。
2. AutoMapper
AutoMapper 使用表达式树来动态生成对象映射代码。它通过构建表达式树来将源对象的属性映射到目标对象的属性,而不是通过反射或手写代码进行属性复制。这样可以提高性能,因为表达式树编译后执行比反射要快得多。
1 | var config = new MapperConfiguration(cfg => { |
AutoMapper 会为每个映射生成表达式树,而不是通过反射获取属性值。这是通过 CreateMap 方法中的 ForMember 配置来实现的。AutoMapper 解析配置,并使用表达式树生成属性映射的代码。
3. FluentValidation
FluentValidation 是一个用于 .NET 的验证库,它使用表达式树来优化验证规则的构建。它使用表达式树动态构建验证逻辑,避免了使用反射进行字段访问,因此提高了性能。
1 | public class UserValidator : AbstractValidator<User> |
FluentValidation 会使用表达式树来定义规则的逻辑,比如 RuleFor(user => user.Name)。在执行验证时,FluentValidation 会解析表达式树并执行相应的验证。
4. Castle DynamicProxy
Castle DynamicProxy 是一个非常流行的开源库,广泛应用于 .NET 中进行动态代理生成。它通过反射和表达式树来动态创建代理类,从而插入方法的拦截逻辑。典型的应用场景是 AOP(面向切面编程),比如日志记录、事务管理、缓存等。
你可以使用 Castle DynamicProxy 来为类或接口动态生成代理对象,并在方法调用前后插入自定义的行为(如日志、缓存、事务等)。这种方式通过表达式树来动态构建代码,从而修改方法的行为。
5. LazyCache
LazyCache 是一个简单而高效的缓存库,它利用延迟加载(Lazy Loading)来优化缓存存取。在缓存未命中时,它会动态加载数据并存储在缓存中,而在缓存命中时直接返回缓存数据。这个过程常常利用表达式树来动态计算值或延迟执行。
延迟计算和缓存可以通过 表达式树 动态生成用于计算的代码,然后在第一次请求时计算,之后缓存结果以避免重复计算。
表达式树VS抽象语法树
表达式树(Expression Tree) 和 抽象语法树(AST,Abstract Syntax Tree) 都是用来表示源代码结构的树形数据结构。它们有很多相似之处,但在功能、使用场景和表达能力上也有一些关键的区别。理解它们的关系和不同点,有助于更好地应用这两种结构。
表达式树 和 抽象语法树 都是 树形数据结构,它们的基本概念和构建方式非常相似。每个树的节点代表程序中的一个操作(如变量、操作符、函数调用等),而每条边则表示这些操作之间的关系。
- 表达式树:专门用于表示程序中的 计算表达式,例如数学运算、函数调用、比较等。
- 抽象语法树:表示程序中的 所有语法结构,包括语句、表达式、控制流结构等。
表达式树与抽象语法树的区别
| 特性 | 抽象语法树(AST) | 表达式树(Expression Tree) |
|---|---|---|
| 表示的内容 | 表示源代码中的 所有语法元素,包括表达式、语句、声明、控制流等。 | 主要表示 表达式,例如数学计算、函数调用等。 |
| 使用场景 | 用于编译器的语法分析和优化过程,可以表示完整的代码结构。 | 主要用于 运行时操作,例如动态生成、执行表达式等。 |
| 处理的结构 | 包括 语句(if、while、for 等控制流结构)、表达式、声明等。 | 仅仅表示 表达式(如加法、减法、函数调用等)。 |
| 生成的目标 | 用于 编译、生成中间语言、优化代码等。 | 用于 动态计算,通常通过编译为委托或直接执行。 |
| 控制流支持 | 支持完整的 控制流(if、while、for 等),适合表示程序的逻辑流程。 | 仅能表示 表达式 中的控制结构,不能完整描述控制流(仅限简单的 if、try-catch 等)。 |
| 优化能力 | 支持对整个代码结构进行优化(常量传播、死代码删除等)。 | 主要支持对表达式的优化(常量折叠、表达式合并等)。 |
- 表达式树(Expression Tree)和 抽象语法树(AST)都是程序的 树形数据结构,用于表示代码的结构。
- AST:表示程序中的所有语法元素,包括语句、控制流、表达式等,主要用于 编译器 的语法分析和优化,支持 完整的控制流。
- 表达式树:专注于表示计算表达式部分,常用于 动态计算,支持优化和执行特定的表达式,不处理控制流和语句。
如何理解两者的关系?
- 关系:可以认为表达式树是抽象语法树的一个 子集,它专注于表示 计算表达式 部分,而抽象语法树则表示程序中的 所有语法结构,包括表达式、语句、控制流、声明等。
- 不同:表达式树不能像 AST 那样表示完整的程序结构和控制流,它仅限于计算部分。因此,表达式树通常用于 动态执行 和 优化计算表达式,而 AST 主要用于 静态分析 和 编译优化。
表达式树基于编译器用于分析代码和生成已编译输出的相同结构
表达式树 提供了代码中 计算表达式 的抽象表示,类似于编译器生成的 抽象语法树(AST),但它的目的是在 运行时动态生成和执行 代码。
编译器将源代码转化为抽象语法树(AST)进行 分析和优化,然后生成中间代码或机器码;而表达式树可以通过
Compile()编译成 可执行的委托,并在运行时执行计算。表达式树 和 编译器生成的中间表示(IR) 在某种程度上是类似的结构,它们都是对代码的 抽象表示,但表达式树的主要目标是 动态执行 计算,而编译器的目标是 生成最终可执行代码。
表达式树和编译器生成输出之间的相似性
假设我们有一个简单的算术表达式:
1 | int result = 5 + 10 * 2; |
- 通过编译器的角度
在编译过程中,编译器首先会将这段源代码转换为 抽象语法树(AST),它的结构类似于:这个 AST 展示了 加法 运算的树形结构,其中的节点包括加法操作符 (1
2
3
4
5+
/ \
5 *
/ \
10 2+) 和乘法操作符 (*) 以及常量值5、10和2。编译器会基于 AST 生成 中间代码,然后进一步优化并生成机器码。 - 通过表达式树的角度
在运行时,我们可以通过 表达式树 来表示这个算术表达式。我们可以使用 C# 中的Expression类来创建这个表达式树:在这个示例中,我们构建了与上述 AST 类似的 表达式树,然后将其编译为 委托 并执行。最终,程序会输出1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22using System;
using System.Linq.Expressions;
public class Program
{
public static void Main()
{
// 构建表达式树 (5 + 10 * 2)
var left = Expression.Constant(5); // 5
var right = Expression.Multiply(Expression.Constant(10), Expression.Constant(2)); // 10 * 2
var add = Expression.Add(left, right); // 5 + (10 * 2)
// 编译表达式树为委托
var lambda = Expression.Lambda<Func<int>>(add);
var compiled = lambda.Compile(); // 生成委托并执行
// 执行表达式树
int result = compiled();
Console.WriteLine(result); // 输出 25
}
}25,这是5 + 10 * 2的计算结果。
编译器分析代码并生成相同结构:编译器将源代码
5 + 10 * 2转换成抽象语法树(AST),这是编译器分析源代码的一个步骤。AST 是对源代码的一种 结构化的抽象表示,它不包含任何执行逻辑,仅仅是表示代码结构。表达式树的作用:表达式树是源代码的 运行时表示,在这个例子中,它代表了一个计算表达式(
5 + 10 * 2)。这个表达式树与编译器生成的 AST 在结构上是相似的,只不过表达式树可以在运行时 动态执行,而 AST 主要用于编译器的 语法分析和优化。表达式树的编译与执行:就像编译器将源代码编译成机器码一样,表达式树可以通过
Expression.Compile()编译成一个可执行的 委托,然后调用这个委托来计算表达式的值。这样,表达式树实现了类似于编译器生成目标代码的过程,只不过它是在 运行时 动态生成和执行的。