表达式树编译版每次调用分配新委托,普通Lambda则不——原因及缓存方法?
表达式树每次调用生成新委托的原因及缓存方案
问题场景
以下表达式树实现的代码,每次调用outer时都会创建新的委托实例,可以通过在Delegate.CreateDelegateNoSecurityCheck断点观察到这一现象:
var param = Expression.Parameter(typeof(int)); var innerParam = Expression.Parameter(typeof(Action<int>)); var inner = Expression.Lambda(innerParam.Type, Expression.Block(), "test", new[] { param }); var outer = Expression.Lambda<Action<int>>(Expression.Block(new[] { innerParam }, Expression.Assign(innerParam, inner), Expression.Invoke(innerParam, param)), param).Compile();
而对应的非表达式树等效代码,重复调用outer不会产生新的委托分配:
Action<int> outer = x => { Action<int> innerParam = _ => { }; innerParam(x); };
疑问:这是预期行为吗?有没有办法缓存表达式树版本的委托以避免重复分配?
背景:该问题源于使用外部序列化库时,进程因委托重复创建产生了不合理的内存分配——该库通过表达式树创建反序列化器,并将委托分配给局部变量以支持递归和循环反序列化。
原因分析(是预期行为)
- 非表达式树代码的优化:C#编译器会对无捕获外部变量的lambda表达式(如示例中的
_ => {})进行优化,将其编译为静态字段存储的委托实例。每次调用outer时,直接复用这个静态实例,不会重复分配内存。 - 表达式树的运行时行为:在表达式树实现中,
inner是一个LambdaExpression节点。当outer执行到Expression.Assign(innerParam, inner)步骤时,运行时会将该LambdaExpression动态编译为新的委托实例——表达式树运行时不会自动缓存这类动态生成的委托,因此每次执行都会触发新的委托创建。
缓存委托的实现方案
方案1:提前编译并嵌入常量委托
如果内部委托的逻辑固定(无动态依赖),可以提前编译好委托实例,然后用Expression.Constant将其嵌入表达式树,避免每次执行时重新编译:
var param = Expression.Parameter(typeof(int)); // 提前编译内部委托,仅执行一次 var innerDelegate = (Action<int>)(_ => { }); var innerParam = Expression.Parameter(typeof(Action<int>)); var outer = Expression.Lambda<Action<int>>( Expression.Block( new[] { innerParam }, // 直接赋值预编译的委托实例 Expression.Assign(innerParam, Expression.Constant(innerDelegate)), Expression.Invoke(innerParam, param) ), param).Compile();
方案2:静态字段缓存动态委托
如果内部委托的逻辑依赖外部参数(无法提前编译),可以通过静态字段实现缓存逻辑,在表达式树中加入“先取缓存,无缓存则创建并存储”的分支:
// 先定义用于缓存的静态字段 private static Action<int> _innerDelegateCache; var param = Expression.Parameter(typeof(int)); var innerParam = Expression.Parameter(typeof(Action<int>)); // 获取缓存字段的反射信息 var cacheField = typeof(YourClass).GetField("_innerDelegateCache", BindingFlags.Static | BindingFlags.NonPublic); // 构建"获取或创建委托"的表达式 var getOrCreateInner = Expression.Condition( Expression.NotEqual(Expression.Field(null, cacheField), Expression.Constant(null)), Expression.Field(null, cacheField), Expression.Assign( Expression.Field(null, cacheField), Expression.Lambda(typeof(Action<int>), Expression.Block(), param) ) ); var outer = Expression.Lambda<Action<int>>( Expression.Block( new[] { innerParam }, Expression.Assign(innerParam, getOrCreateInner), Expression.Invoke(innerParam, param) ), param).Compile();
注:将YourClass替换为实际包含静态字段的类名。
内容的提问来源于stack exchange,提问作者Bogey
相关产品推荐
相关产品推荐

