表达式树(简述)
本教程共 100 篇 · 第 60 篇 · 更新于 2026-07-31 · 约 9 分钟阅读
60. 表达式树(简述)
本节目标:学完你知道 Expression<Func<>> 是什么、它怎么支撑 LINQ to SQL,以及和委托有何不同。
前面两章的 Lambda 都变成了「能执行的委托」。可有时我们不想立刻执行逻辑,而想「看懂」这段逻辑长什么样——比如翻译成 SQL 发给数据库。
表达式树(expression tree) 干的就是这个:把 Lambda 变成一棵「可检视的数据结构」,而不是可调用的方法。
一、Expression<Func<>> 长什么样
同样的 Lambda,套上 Expression<> 就不是委托了:
namespace CSharpDemo;
using System.Linq.Expressions;
// 这是委托:可被调用
Func<int, int> del = x => x * 2;
// 这是表达式树:是可分析的数据,不是方法
Expression<Func<int, int>> expr = x => x * 2;
Console.WriteLine(del(5)); // 10,直接执行
Console.WriteLine(expr.Compile()(5)); // 10,先编译成委托再执行
del(5) 立刻算出 10。而 expr 本身不执行;只有调用 expr.Compile() 把它编译成委托,才能跑。
Note普通代码里几乎用不到表达式树。它主要被框架(如 LINQ to SQL、Entity Framework)在背后使用。入门了解即可。
二、为什么需要它:LINQ to SQL 思路
想象你要从数据库查「年龄大于 18 的人」。用 LINQ 写:
var adults = db.People.Where(p => p.Age > 18);
Where 收到的若是普通委托,它只能「在内存里一个个调」。但数据库不在内存里,没法直接调方法。
于是 Where 把 Lambda 收成 Expression<Func<>>,拆成树:参数 p、成员 Age、常量 18、运算符 >。框架遍历这棵树,翻译成 SQL:WHERE Age > 18,发给数据库执行。
Tip这就是「表达式树」的价值:它让逻辑「可见、可翻译」。委托是「做」,表达式树是「描述怎么做」。
三、和委托的核心区别
| 维度 | 委托(Func) | 表达式树(Expression) |
|---|---|---|
| 本质 | 可调用的方法 | 描述逻辑的数据结构 |
| 能直接执行吗 | 能,f(5) | 不能,需先 Compile() |
| 能被框架拆解翻译吗 | 不能 | 能 |
| 典型用途 | 内存里传逻辑 | LINQ to SQL / 动态查询 |
一句话:委托用来「跑」,表达式树用来「读」。
Note两者方向是单向的:表达式树可以通过
Compile()变成委托去执行;反过来,一个已经编译好的委托「丢掉」了结构信息,无法还原成表达式树。所以「描述」可以转成「执行」,「执行」无法退回成「描述」。
四、表达式树能拆到什么程度
你可以自己遍历它,看看里面有什么:
namespace CSharpDemo;
using System.Linq.Expressions;
Expression<Func<int, int>> e = x => x * 2;
Console.WriteLine(e.Body); // (x * 2)
Console.WriteLine(e.Parameters[0].Name); // x
e.Body 是「乘 2」这个运算节点,e.Parameters 是参数列表。整棵树就是「节点 + 子节点」的结构,框架正是这样逐层读懂你的意图。
五、不是所有 Lambda 都能变表达式树
限制一:表达式树只接受「表达式体」Lambda,不能有大括号语句体、return 或多条语句。
// 合法:单表达式
Expression<Func<int, int>> ok = x => x * 2;
// 不合法:语句体不能转表达式树
// Expression<Func<int, int>> bad = x => { return x * 2; };
限制二:它不能捕获「可变、带副作用」的复杂外部状态,否则无法干净翻译成 SQL。这正符合数据库查询「应是纯描述」的直觉。
Note日常写 LINQ to Objects(对内存集合)时,用的是
Func委托,直接执行;只有对「可远程查询源」(如数据库)才用到表达式树。别混淆两者。
六、什么时候你会碰到它
普通业务代码极少手写 Expression<>。但当你用 Entity Framework、写动态筛选条件、或做规则引擎时,往往会「无意间」用到——因为 Where 的重载签名期望的就是表达式树。
Tip入门阶段只要记住:看到
Expression<Func<...>>参数,就明白「这个 Lambda 将被拆解翻译,而不是就地执行」。理解这一层,读框架文档就不慌了。
七、机制背后:IQueryable 与 IEnumerable
为什么有的 LINQ 走委托、有的走表达式树?关键在返回类型。
IEnumerable<T>:数据在内存里,LINQ 用Func委托当场执行,比如List<T>的查询。IQueryable<T>:数据在别处(数据库、远程服务),LINQ 把 Lambda 收成Expression,交给「提供程序(provider)」翻译成对方能懂的语言再执行。
namespace CSharpDemo;
List<int> local = [1, 2, 3];
// IEnumerable 路径:Where 收的是 Func,内存里立即跑
var r1 = local.Where(x => x > 1);
// 若 source 是 IQueryable(如 EF 的 DbSet):
// var r2 = db.Users.Where(u => u.Age > 18);
// 这里 Where 收的是 Expression<Func<...>>,被翻译成 SQL
同一句 Where(x => x > 1),落在 IEnumerable 上就地算,落在 IQueryable 上被拆解翻译。你写代码时语法一样,差异全在底层类型。
Note所以「表达式树」不是你主动选的,而是当你操作
IQueryable数据源时,框架自动替你收成了表达式树。理解这点,就明白为什么数据库查询有时「看不懂」你在 Lambda 里写的某些 C# 方法。
八、表达式树做不到什么
因为表达式树最终要被翻译成 SQL 之类的外部语言,它能表达的只是「数据查询」这一小撮逻辑:
- 不能含 C# 专属、数据库没有对应实现的复杂方法(比如调用本地自定义函数、读文件)。
- 不能有副作用(赋值、打印、抛异常都不行)。
- 只能是「描述数据怎么筛 / 怎么投影」,不能是一串任意语句。
一旦你在 Lambda 里写了数据库翻译不了的东西,IQueryable 查询会在运行时报错。这也是为什么表达式树比委托「受限」得多。
Tip写数据库查询时,尽量让 Lambda 保持「纯表达式」:只做比较、算数、属性访问。想做复杂加工,先取回内存(用
AsEnumerable())再用委托处理。
九、表达式树能手工拼出来吗
除了让编译器从 Lambda 生成,你也能用 API 手工搭一棵树。这正是框架背后做的事——把「节点」逐个拼起来:
namespace CSharpDemo;
using System.Linq.Expressions;
var param = Expression.Parameter(typeof(int), "x");
var constant = Expression.Constant(18);
var body = Expression.GreaterThan(param, constant);
var lambda = Expression.Lambda<Func<int, bool>>(body, param);
Console.WriteLine(lambda); // x => (x > 18)
Expression.Parameter 造参数节点,Expression.Constant 造常量节点,Expression.GreaterThan 把两者拼成「大于」运算节点,最后 Expression.Lambda 收成一个完整的 Lambda 树。手工拼树适合做动态查询(比如按用户勾选的条件临时组合 Where),但入门阶段知道「树是这么来的」就够,不必现在就上手拼。
十、动手练一练
- 写
Expression<Func<int, int>> e = x => x + 1;,打印e.Body和e.Parameters[0].Name,感受「树」的结构。 - 用
e.Compile()(9)把它编译成委托并执行,验证表达式树能转回委托。 - 对比两句
Where:一句对List<int>(得到IEnumerable),一句想象它来自数据库IQueryable,说出两者分别走委托还是表达式树。 - 在
IQueryable的Where里故意写p => p.Name.ToUpper().Contains("A"),思考哪些调用能被翻译成 SQL;再写一个「调用本地自定义方法」的 Lambda,体会为什么框架会翻译失败。
十一、小结
Expression<Func<>> 把 Lambda 存成一棵「可读取的逻辑树」,而非可调用方法。它让 LINQ to SQL 能把 p => p.Age > 18 翻译成 WHERE Age > 18。委托负责「执行」(走 IEnumerable),表达式树负责「描述」(走 IQueryable 再翻译)。两者单向可转换:树能 Compile() 成委托,委托无法退回成树。表达式树受限于「可翻译」,是框架的利器,入门了解其存在、用途与边界即可。至此,委托、事件、Lambda、闭包、表达式树这条「把逻辑当数据」的主线就串完了。