C# 表达式树 Expression 基础讲解:原理、创建与使用

洛溪
洛溪
发布时间:2026-10-16 | 7人浏览过
原创

本文详解 C# 表达式树 (Expression) 的原理、结构及其在 LINQ 和 Entity Framework Core 中的应用。重点解析 Expression 与 Func 的性能差异,避免内存过滤陷阱,并提供通过代码手动构建表达式树的完整步骤与示例。

什么是表达式树

表达式树(Expression Tree)以树形数据结构表示代码逻辑,其中每一个节点代表一种表达式,例如二元运算、方法调用等。通过表达式树,开发者可以动态修改可执行代码、生成动态 SQL 查询语句,或者在 .NET 与动态语言之间建立互操作性。

在 C# 中,表达式树的核心在于它将代码逻辑视为“数据”。我们可以先创建一个简单的 Lambda 表达式:

[HttpGet]
public IActionResult Expression()
{
    // 定义一个查询条件:查找年龄大于 18 的用户
    Expression<Func<User, bool>> expression = x => x.Age > 18;
    return Ok();
}

对于上述的 x.Age > 18 表达式,系统内部将其转换为如下的树状结构:

如果在 Visual Studio 中使用“监视窗口”查看该变量的属性,可以观察到:根节点(NodeType)是 GreaterThan;左子节点(Left)代表访问属性操作(即 x.Age);右子节点(Right)代表常量值 18。基于这些节点信息,我们可以清晰地还原出表达式树的层级关系。

这种树状结构使得底层框架能够解析代码语义,进而将表达式编译为具体的 SQL 查询语句或执行引擎能识别的逻辑。

Expression 与 Func 的本质区别

虽然两者都可用于定义委托,但在处理数据库查询时存在显著差异:

  • Expression (表达式树):存储的是运算逻辑的抽象语法树(AST)。它保留了完整的数学和逻辑结构,支持在运行时被遍历、分析和转换。
  • Func (普通委托):仅存储一段可在当前环境执行的机器码或 IL 指令。它不保存语法树结构,无法被外部工具解析。

因此,在使用 Entity Framework Core 进行数据库查询时,必须选择 Expression 而非 Func:

// 【正确写法】使用 Expression
// EF Core 会解析树结构,将其转换为 SQL:SELECT * FROM User WHERE Age > 18
Expression<Func<User, bool>> expression1 = x => x.Age > 18;
var result1 = dbContext.Users.Where(expression1).ToList();

// 【错误写法】使用 Func
// EF Core 无法将其转换为 SQL,会执行 SELECT * FROM User,将所有数据拉取到内存后,再由 Linq to Objects 过滤
Func<User, bool> func1 = x => x.Age > 18;
var result2 = dbContext.Users.Where(func1).ToList();

简而言之,使用 Func 会导致全表扫描并在内存中进行过滤,这会严重降低性能并增加网络开销。而使用 Expression 可以将过滤逻辑推送到数据库层执行。

手动通过代码创建表达式树

除了使用 Lambda 表达式语法,我们还可以利用 System.Linq.Expressions 命名空间下的工厂方法来逐行构建表达式树。常见的类包括:

  • ParameterExpression:参数变量(如 lambda 中的 x)。
  • ConstantExpression:常量值(如 18)。
  • MemberExpression:成员访问(如 x.Age)。
  • BinaryExpression:二元运算(如 > 比较)。
  • LambdaExpression:组合整个表达式的根节点。

由于这些类的构造函数通常是内部公开的,我们不能直接 new 它们,必须调用 Expression.Parameter, Expression.Constant, Expression.MakeMemberAccess, Expression.GreaterThanOrEqual 以及 Expression.Lambda 等静态方法。

第1步:创建参数节点

首先创建表达式的输入参数,指定类型和名称。

// 对应 Lambda 表达式中的 'u' 变量
ParameterExpression parameterExpression = Expression.Parameter(typeof(User), "u");

第2步:构建叶子节点(左、右子节点)

按照运算符的结合方向,先构建子表达式:

  • 右节点:常量 18。
  • 左节点:成员访问操作,获取 User 的 Age 属性。
// 右节点:值为 18 的常量
ConstantExpression right = Expression.Constant(18);

// 左节点:User.属性(Age)
MemberExpression left = Expression.MakeMemberAccess(parameterExpression, typeof(User).GetProperty("Age"));

第3步:组合成主体节点 (Body)

使用二元表达式类将左右节点连接起来,表示 “>=” 比较:

// 左 >= 右
BinaryExpression body = Expression.GreaterThanOrEqual(left, right);

第4步:组装最终 Lambda 表达式

将主体和参数放入 Lambda 节点中,完成整个表达式的构建:

Expression<Func<User, bool>> expression = Expression.Lambda<Func<User, bool>>(body, parameterExpression);

以上代码片段展示了如何在不写 Lambda 符号的情况下,从底层 API 一步步拼装出完全相同的表达式树结构。这在需要高度动态生成复杂查询条件的场景中非常有用。

以上就是C#表达式树 Expression 基础讲解的详细内容,更多关于C#高级特性的资料请关注本站其它相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表