C#.Net-表达式目录树-学习笔记
一、本质与核心概念
表达式目录树是一种数据结构,以树形结构描述代码逻辑。它不是可执行代码,而是对代码结构的描述。
- 委托(Delegate):编译后直接生成 IL,可以直接执行
- 表达式目录树(Expression Tree):是一个树形数据对象,描述了计算关系,不能直接执行
Expression<Func<T>>vsFunc<T>:前者是表达式树,后者是委托,二者可通过.Compile()单向转换- 语法限制:表达式目录树只能是单行 Lambda,不能有大括号和多行语句
- 树形结构:类似二叉树,每个节点都是一个子表达式,可以一层层拆解或拼装
csharp
// 委托:直接可执行
Func<int, int, int> func = (m, n) => m * n + 2;
// 表达式目录树:描述结构,需要 Compile() 后才能执行
Expression<Func<int, int, int>> exp = (m, n) => m * n + 2;
Func<int, int, int> compiled = exp.Compile();
int result = compiled(10, 20); // 202
表达式 (m, n) => m * n + 2 的树形结构:
+(加法)
/ \
* 2(常量)
/ \
m n(参数)
二、动态拼装表达式目录树
拼装的核心思路:把最小粒度的节点逐步组合成完整表达式,最后包裹成 Lambda。
2.1 基础:无参数
csharp
// 目标:() => 123 + 234
ConstantExpression c1 = Expression.Constant(123);
ConstantExpression c2 = Expression.Constant(234);
BinaryExpression add = Expression.Add(c1, c2);
Expression<Func<int>> lambda = Expression.Lambda<Func<int>>(add);
int result = lambda.Compile()(); // 357
2.2 带参数
csharp
// 目标:m => m + 1
ParameterExpression paramM = Expression.Parameter(typeof(int), "m");
ConstantExpression c1 = Expression.Constant(1);
BinaryExpression add = Expression.Add(paramM, c1);
Expression<Func<int, int>> lambda = Expression.Lambda<Func<int, int>>(add, paramM);
int result = lambda.Compile()(5); // 6
2.3 访问属性/字段
csharp
// 目标:c => c.Id == 10
ParameterExpression paramC = Expression.Parameter(typeof(People), "c");
// 字段用 Expression.Field,属性用 Expression.Property
FieldInfo fieldId = typeof(People).GetField("Id");
MemberExpression idExp = Expression.Field(paramC, fieldId);
ConstantExpression c10 = Expression.Constant(10);
BinaryExpression equal = Expression.Equal(idExp, c10);
Expression<Func<People, bool>> lambda =
Expression.Lambda<Func<People, bool>>(equal, paramC);
2.4 调用方法
csharp
// 目标:c => c.Name.Equals("Richard")
ParameterExpression paramC = Expression.Parameter(typeof(People), "c");
PropertyInfo propName = typeof(People).GetProperty("Name");
MemberExpression nameExp = Expression.Property(paramC, propName);
MethodInfo equalsMethod = typeof(string).GetMethod("Equals", new[] { typeof(string) });
ConstantExpression constRichard = Expression.Constant("Richard");
MethodCallExpression callExp = Expression.Call(nameExp, equalsMethod, constRichard);
// 包装成 Lambda 并执行
Expression<Func<People, bool>> lambda =
Expression.Lambda<Func<People, bool>>(callExp, paramC);
bool result = lambda.Compile()(new People { Name = "Richard" }); // true
2.5 复杂表达式:建议从右往左拼装
csharp
// 目标:c => c.Id.ToString() == "10" && c.Name.Equals("Richard") && c.Age > 35
ParameterExpression paramC = Expression.Parameter(typeof(People), "c");
// 1. c.Age > 35
var ageExp = Expression.Property(paramC, typeof(People).GetProperty("Age"));
var ageGreater = Expression.GreaterThan(ageExp, Expression.Constant(35));
// 2. c.Name.Equals("Richard")
var nameExp = Expression.Property(paramC, typeof(People).GetProperty("Name"));
var equalsMethod = typeof(string).GetMethod("Equals", new[] { typeof(string) });
var nameEquals = Expression.Call(nameExp, equalsMethod, Expression.Constant("Richard"));
// 3. c.Id.ToString() == "10"
var idExp = Expression.Field(paramC, typeof(People).GetField("Id"));
var toStringMethod = typeof(int).GetMethod("ToString", Type.EmptyTypes);
var toStringExp = Expression.Call(idExp, toStringMethod);
var idEqual = Expression.Equal(toStringExp, Expression.Constant("10"));
// 4. 用 AndAlso 连接(对应 &&)
var and1 = Expression.AndAlso(idEqual, nameEquals);
var finalExp = Expression.AndAlso(and1, ageGreater);
Expression<Func<People, bool>> lambda =
Expression.Lambda<Func<People, bool>>(finalExp, paramC);
技巧:遇到复杂表达式,可以先用快捷 Lambda 写出来,再用 ILSpy 等反编译工具查看中间语言,照着拼装。
三、表达式访问者(ExpressionVisitor)
3.1 核心机制
ExpressionVisitor 是遍历和修改表达式目录树的工具,采用访问者模式。
Visit(expression):入口方法,判断节点类型后分发到对应的VisitXxx方法- 表达式树是二叉树结构,
Visit会递归遍历到所有叶节点 - 重写
VisitXxx方法可以在任意节点拦截、读取或修改内容 - 返回新节点即可修改表达式树,返回原节点则保持不变
3.2 修改表达式树(OperationsVisitor)
通过重写 VisitBinary,可以在遍历时把加法替换成减法、乘法替换成除法:
csharp
public class OperationsVisitor : ExpressionVisitor
{
public Expression Modify(Expression expression)
{
return Visit(expression);
}
protected override Expression VisitBinary(BinaryExpression b)
{
if (b.NodeType == ExpressionType.Add)
{
Expression left = Visit(b.Left);
Expression right = Visit(b.Right);
return Expression.Subtract(left, right); // 加法 → 减法
}
if (b.NodeType == ExpressionType.Multiply)
{
Expression left = Visit(b.Left);
Expression right = Visit(b.Right);
return Expression.Divide(left, right); // 乘法 → 除法
}
return base.VisitBinary(b);
}
}
// 使用
Expression<Func<int, int, int>> exp = (m, n) => m * n + 2;
var visitor = new OperationsVisitor();
Expression modified = visitor.Modify(exp);
// 原来 m * n + 2,修改后变成 m / n - 2
3.3 解析表达式为 SQL(ConditionBuilderVisitor)
用栈来收集每个节点转换出的 SQL 片段,最终拼接成完整条件:
csharp
public class ConditionBuilderVisitor : ExpressionVisitor
{
private Stack<string> _StringStack = new Stack<string>();
public string Condition()
{
string condition = string.Concat(_StringStack.ToArray());
_StringStack.Clear();
return condition;
}
// 二元表达式:a > b、a && b 等
protected override Expression VisitBinary(BinaryExpression node)
{
_StringStack.Push(")");
Visit(node.Right);
// ToSqlOperator() 是自定义扩展方法,将 ExpressionType 映射为 SQL 运算符
// 如 AndAlso/And → "AND",Equal → "=",GreaterThan → ">" 等(见本章末尾映射表)
_StringStack.Push(" " + node.NodeType.ToSqlOperator() + " ");
Visit(node.Left);
_StringStack.Push("(");
return node;
}
// 成员访问:c.Age、c.Name
// 注意:如果是外部变量(闭包捕获),需要通过反射取值,而不是直接输出字段名
protected override Expression VisitMember(MemberExpression node)
{
if (node.Expression is ConstantExpression)
{
// 外部变量:通过反射获取实际值
var obj = (node.Expression as ConstantExpression).Value;
var value = (node.Member as FieldInfo).GetValue(obj);
_StringStack.Push($"'{value}'");
}
else
{
// 实体属性:输出列名
_StringStack.Push($" [{node.Member.Name}] ");
}
return node;
}
// 常量:10、"abc"
protected override Expression VisitConstant(ConstantExpression node)
{
_StringStack.Push($" '{node.Value}' ");
return node;
}
// 方法调用:Contains、StartsWith、EndsWith
protected override Expression VisitMethodCall(MethodCallExpression m)
{
string format = m.Method.Name switch
{
"StartsWith" => "({0} LIKE {1}+'%')",
"Contains" => "({0} LIKE '%'+{1}+'%')",
"EndsWith" => "({0} LIKE '%'+{1})",
_ => throw new NotSupportedException(m.NodeType + " is not supported!")
};
Visit(m.Object);
Visit(m.Arguments[0]);
string right = _StringStack.Pop();
string left = _StringStack.Pop();
_StringStack.Push(string.Format(format, left, right));
return m;
}
}
ExpressionType 到 SQL 运算符的映射(SqlOperator):
| ExpressionType | SQL |
|---|---|
AndAlso / And |
AND |
OrElse / Or |
OR |
Equal |
= |
NotEqual |
<> |
GreaterThan |
> |
GreaterThanOrEqual |
>= |
LessThan |
< |
LessThanOrEqual |
<= |
使用示例:
csharp
Expression<Func<People, bool>> lambda = x =>
x.Age > 5
&& x.Name.StartsWith("A")
&& x.Name.Contains("B")
&& x.Name.EndsWith("C");
var visitor = new ConditionBuilderVisitor();
visitor.Visit(lambda);
Console.WriteLine(visitor.Condition());
// 输出: ((( [Age] > '5') AND ([Name] LIKE 'A'+'%')) AND ([Name] LIKE '%'+'B'+'%')) AND ([Name] LIKE '%'+'C'))
外部变量捕获的处理:
csharp
string name = "AAA";
Expression<Func<People, bool>> lambda = x => x.Age > 5 && x.Name == name || x.Id > 5;
// name 是外部变量,编译后会被包装成 ConstantExpression 的字段
// VisitMember 中检测到 node.Expression is ConstantExpression,通过反射取出 "AAA"
3.4 实战扩展:BatchDelete
将表达式树解析为 SQL WHERE 条件,直接用于数据库操作:
csharp
public static void BatchDelete<T>(this IQueryable<T> entities, Expression<Func<T, bool>> expr)
{
var visitor = new ConditionBuilderVisitor();
visitor.Visit(expr);
string condition = visitor.Condition();
string sql = $"DELETE FROM [{typeof(T).Name}] WHERE {condition}";
// 执行 sql...
}
// 使用
dbSet.BatchDelete<People>(p => p.Age > 30 && p.Name.StartsWith("A"));
// 生成: DELETE FROM [People] WHERE (( [Age] > '30') AND ([Name] LIKE 'A'+'%'))
四、表达式扩展:And / Or / Not
4.1 为什么需要参数替换
两个独立声明的表达式,参数对象不是同一个实例,直接合并会报错。需要用 ExpressionVisitor 统一替换成同一个参数:
csharp
Expression<Func<People, bool>> exp1 = x => x.Age > 5; // 参数是 x
Expression<Func<People, bool>> exp2 = y => y.Id > 5; // 参数是 y,不同实例
// 直接 Expression.AndAlso(exp1.Body, exp2.Body) 会出错
// 需要把两个 Body 中的参数都替换成同一个新参数 c
4.2 参数替换访问者(NewExpressionVisitor)
csharp
internal class NewExpressionVisitor : ExpressionVisitor
{
private readonly ParameterExpression _newParameter;
public NewExpressionVisitor(ParameterExpression param)
{
_newParameter = param;
}
public Expression Replace(Expression exp) => Visit(exp);
protected override Expression VisitParameter(ParameterExpression node)
{
return _newParameter; // 把所有参数节点替换成新参数
}
}
4.3 And / Or / Not 扩展方法
csharp
public static class ExpressionExtend
{
public static Expression<Func<T, bool>> And<T>(
this Expression<Func<T, bool>> expr1,
Expression<Func<T, bool>> expr2)
{
if (expr1 == null) return expr2;
if (expr2 == null) return expr1;
ParameterExpression param = Expression.Parameter(typeof(T), "c");
var visitor = new NewExpressionVisitor(param);
var left = visitor.Replace(expr1.Body);
var right = visitor.Replace(expr2.Body);
var body = Expression.And(left, right); // 位运算 &,非短路
return Expression.Lambda<Func<T, bool>>(body, param);
}
public static Expression<Func<T, bool>> Or<T>(
this Expression<Func<T, bool>> expr1,
Expression<Func<T, bool>> expr2)
{
ParameterExpression param = Expression.Parameter(typeof(T), "c");
var visitor = new NewExpressionVisitor(param);
var left = visitor.Replace(expr1.Body);
var right = visitor.Replace(expr2.Body);
var body = Expression.Or(left, right); // 位运算 |,非短路
return Expression.Lambda<Func<T, bool>>(body, param);
}
public static Expression<Func<T, bool>> Not<T>(
this Expression<Func<T, bool>> expr)
{
var body = Expression.Not(expr.Body);
return Expression.Lambda<Func<T, bool>>(body, expr.Parameters[0]);
}
}
注意:
Expression.And对应位运算&(非短路),Expression.AndAlso对应&&(短路)。源码中And扩展方法用的是Expression.And,Or用的是Expression.Or。
使用示例:
csharp
Expression<Func<People, bool>> exp1 = x => x.Age > 5;
Expression<Func<People, bool>> exp2 = x => x.Id > 5;
var andExp = exp1.And(exp2); // c => c.Age > 5 & c.Id > 5
var orExp = exp1.Or(exp2); // c => c.Age > 5 | c.Id > 5
var notExp = exp1.Not(); // x => !(x.Age > 5)
// 动态构建查询条件
Expression<Func<People, bool>> query = null;
string name = Console.ReadLine();
if (!string.IsNullOrWhiteSpace(name))
query = query.And(p => p.Name.Contains(name));
string ageStr = Console.ReadLine();
if (int.TryParse(ageStr, out int age))
query = query.And(p => p.Age > age);
var result = dbSet.Where(query).ToList();
五、对象映射(高性能 Mapper)
5.1 五种方案对比
| 方案 | 性能 | 灵活性 | 说明 |
|---|---|---|---|
| 硬编码 | ⭐⭐⭐⭐⭐ | ⭐ | 手写赋值,不通用 |
| 反射(ReflectionMapper) | ⭐ | ⭐⭐⭐⭐⭐ | 每次都要反射,慢 |
| 序列化(SerializeMapper) | ⭐ | ⭐⭐⭐⭐ | JSON 序列化再反序列化,慢 |
| 表达式树+字典缓存(ExpressionMapper) | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 首次拼装,后续走字典取委托 |
| 表达式树+泛型缓存(ExpressionGenericMapper) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 最优,泛型副本天然隔离 |
5.2 反射方案(最慢)
csharp
public static TOut Trans<TIn, TOut>(TIn tIn)
{
TOut tOut = Activator.CreateInstance<TOut>();
foreach (var prop in tOut.GetType().GetProperties())
{
var sourceProp = tIn.GetType().GetProperty(prop.Name);
prop.SetValue(tOut, sourceProp.GetValue(tIn));
}
foreach (var field in tOut.GetType().GetFields())
{
var sourceField = tIn.GetType().GetField(field.Name);
field.SetValue(tOut, sourceField.GetValue(tIn));
}
return tOut;
}
5.3 序列化方案(慢)
csharp
public static TOut Trans<TIn, TOut>(TIn tIn)
{
string json = JsonConvert.SerializeObject(tIn);
return JsonConvert.DeserializeObject<TOut>(json);
}
5.4 表达式树+字典缓存(ExpressionMapper)
用 Dictionary<string, object> 缓存编译好的委托,key 是类型组合字符串。首次调用拼装并编译,后续直接从字典取委托执行:
csharp
private static Dictionary<string, object> _Dic = new Dictionary<string, object>();
public static TOut Trans<TIn, TOut>(TIn tIn)
{
string key = $"funckey_{typeof(TIn).FullName}_{typeof(TOut).FullName}";
if (!_Dic.ContainsKey(key))
{
// 拼装表达式:p => new TOut { Prop1 = p.Prop1, ... }
ParameterExpression param = Expression.Parameter(typeof(TIn), "p");
List<MemberBinding> bindings = new List<MemberBinding>();
foreach (var prop in typeof(TOut).GetProperties())
{
var sourceProp = Expression.Property(param, typeof(TIn).GetProperty(prop.Name));
bindings.Add(Expression.Bind(prop, sourceProp));
}
foreach (var field in typeof(TOut).GetFields())
{
var sourceField = Expression.Field(param, typeof(TIn).GetField(field.Name));
bindings.Add(Expression.Bind(field, sourceField));
}
var memberInit = Expression.MemberInit(Expression.New(typeof(TOut)), bindings.ToArray());
var lambda = Expression.Lambda<Func<TIn, TOut>>(memberInit, param);
_Dic[key] = lambda.Compile();
}
return ((Func<TIn, TOut>)_Dic[key]).Invoke(tIn);
}
注意:
_Dic的ContainsKey+ 写入不是原子操作,多线程并发时存在竞态条件。生产环境建议改用ConcurrentDictionary或加锁,或直接使用泛型缓存方案。
5.5 表达式树+泛型缓存(ExpressionGenericMapper,最优)
泛型类的静态字段天然为每组类型生成独立副本,无需字典查找,性能更高:
csharp
public class ExpressionGenericMapper<TIn, TOut>
{
private static readonly Func<TIn, TOut> _FUNC;
static ExpressionGenericMapper()
{
ParameterExpression param = Expression.Parameter(typeof(TIn), "p");
List<MemberBinding> bindings = new List<MemberBinding>();
// 处理属性
foreach (var prop in typeof(TOut).GetProperties())
{
var sourceProp = Expression.Property(param, typeof(TIn).GetProperty(prop.Name));
bindings.Add(Expression.Bind(prop, sourceProp));
}
// 处理字段
foreach (var field in typeof(TOut).GetFields())
{
var sourceField = Expression.Field(param, typeof(TIn).GetField(field.Name));
bindings.Add(Expression.Bind(field, sourceField));
}
var memberInit = Expression.MemberInit(Expression.New(typeof(TOut)), bindings.ToArray());
var lambda = Expression.Lambda<Func<TIn, TOut>>(memberInit, param);
_FUNC = lambda.Compile(); // 只执行一次
}
public static TOut Trans(TIn t) => _FUNC(t);
}
// 使用
PeopleCopy copy = ExpressionGenericMapper<People, PeopleCopy>.Trans(people);
泛型缓存的原理: ExpressionGenericMapper<People, PeopleCopy> 和 ExpressionGenericMapper<User, UserDto> 是两个不同的类,各自有独立的静态字段 _FUNC,CLR 保证每组类型只初始化一次。
六、核心知识点总结
6.1 为什么要用表达式目录树?
- 动态性:在运行时动态构建查询逻辑,把程序写活
- 可分析性:可以解析表达式结构,转换为 SQL、MongoDB 查询、ES DSL 等
- 类型安全:编译时检查,避免字符串拼接 SQL 的错误和注入风险
- 高性能:配合缓存,性能接近硬编码
6.2 常用表达式节点速查
| 类型 | 说明 | 创建示例 |
|---|---|---|
ConstantExpression |
常量 | Expression.Constant(10) |
ParameterExpression |
参数 | Expression.Parameter(typeof(int), "x") |
BinaryExpression |
二元运算(加减乘除、比较、逻辑) | Expression.Add(a, b) / Expression.GreaterThan(a, b) |
MemberExpression |
成员访问(属性/字段) | Expression.Property(obj, "Name") / Expression.Field(obj, fi) |
MethodCallExpression |
方法调用 | Expression.Call(obj, method, args) |
MemberInitExpression |
对象初始化 | Expression.MemberInit(newExp, bindings) |
LambdaExpression |
Lambda 整体 | Expression.Lambda<Func<...>>(body, params) |
6.3 实际应用场景
- ORM 框架:Entity Framework、Dapper 等将 LINQ 表达式树解析为 SQL
- 动态查询:根据用户输入拼装 WHERE 条件,替代字符串拼接 SQL
- 对象映射:AutoMapper 底层原理,动态生成硬编码级别的映射委托
- 规则引擎:运行时动态构建业务规则
- 批量操作扩展:如
BatchDelete<T>直接从表达式生成 DELETE SQL - 代码生成:运行时动态生成高性能执行逻辑
6.4 学习建议
- 理解本质:表达式树是数据结构,不是可执行代码
- 从简单开始:先掌握基础拼装,再学习复杂场景
- 善用反编译:用 ILSpy 等工具查看快捷 Lambda 编译后的中间语言,照着拼装
- 性能意识:合理使用缓存,避免重复拼装;优先选泛型缓存而非字典缓存
记住:表达式目录树 = 动态 + 类型安全 + 高性能