06-C#

C#.Net-表达式目录树-学习笔记

一、本质与核心概念

表达式目录树是一种数据结构,以树形结构描述代码逻辑。它不是可执行代码,而是对代码结构的描述。

  • 委托(Delegate):编译后直接生成 IL,可以直接执行
  • 表达式目录树(Expression Tree):是一个树形数据对象,描述了计算关系,不能直接执行
  • Expression<Func<T>> vs Func<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.AndOr 用的是 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);
}

注意:_DicContainsKey + 写入不是原子操作,多线程并发时存在竞态条件。生产环境建议改用 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 编译后的中间语言,照着拼装
  • 性能意识:合理使用缓存,避免重复拼装;优先选泛型缓存而非字典缓存

记住:表达式目录树 = 动态 + 类型安全 + 高性能

相关推荐
云栖梦泽2 小时前
易语言开发从入门到精通:进阶篇·图形图像高级实战
开发语言
程序员小李白2 小时前
vue2基本语法详细解析(2.7条件渲染)
开发语言·前端·javascript
xyq20242 小时前
Chart.js 安装指南
开发语言
ysa0510302 小时前
模拟【打牌游戏】
数据结构·c++·笔记·算法
Predestination王瀞潞2 小时前
1. Java SE到底是什么:不仅仅是面向对象
java·开发语言
Byron07072 小时前
Python面向对象编程(OOP)详解:类、对象、继承、多态、封装
开发语言·python
ht巷子2 小时前
boost.asio网络学习:Http Server
网络·c++·http
-许平安-2 小时前
MCP项目笔记三(server)
网络·c++·笔记·mcp
weixin_649555672 小时前
C语言程序设计第四版(何钦铭、颜晖)第八章指针之循环后移
c语言·c++·算法