缓存 methodinfo 或用表达式树预编译委托可避开90%以上反射开销;methodinfo.invoke()因类型检查、参数装箱、安全性验证等步骤比直接调用慢30–100倍;expression.compile()生成纯il委托,需固定实例、显式声明参数类型、预先构造泛型闭包并缓存委托;c#12拦截器仅适用于编译期已知调用目标的静态场景,不适用于动态反射;缓存键应避免字符串插值以防隐式分配。

直接缓存 MethodInfo 或用表达式树预编译委托,能避开 90% 以上的反射调用开销。动态调用(dynamic 或 MethodInfo.Invoke())在高频场景下比直接调用慢 30–100 倍,这不是理论值,是真实金融交易系统压测出的数据。
为什么 MethodInfo.Invoke() 慢得明显
每次调用 MethodInfo.Invoke() 都要走完整反射链:类型检查 → 参数装箱/拆箱 → 安全性验证 → 实际跳转。其中参数数组 new object[] { a, b } 必然触发堆分配,GC 压力随之上升。
- 即使缓存了
MethodInfo,Invoke()本身仍是虚方法调用 + 运行时绑定,无法被 JIT 内联 - 泛型方法(如
List<t>.Add()</t>)需运行时构造具体类型,开销进一步放大 - 私有成员访问会额外触发
BindingFlags.NonPublic权限绕过检查,再加一层成本
Expression.Compile() 预编译委托的实操要点
用 Expression 构建调用树后 Compile(),生成的是纯 IL 委托,和手写方法几乎等效。关键不是“会不会写表达式树”,而是怎么写才不踩坑。
- 必须用
Expression.Constant(instance)固定目标对象,否则每次调用都重新解析实例 —— 这就又退回反射了 - 参数用
Expression.Parameter(typeof(int), "a")显式声明类型,避免Convert节点引入隐式装箱 - 对泛型方法,先用
method.MakeGenericMethod(typeof(string))确定具体闭包,再构树;否则Compile()会失败 - 编译后的委托(如
Func<object int></object>)应缓存到静态字典,键可为$"{method.DeclaringType}.{method.Name}"
示例片段:
var instanceParam = Expression.Constant(obj);
var method = typeof(Math).GetMethod("Abs", new[] { typeof(int) });
var call = Expression.Call(instanceParam, method, Expression.Constant(-5));
var func = Expression.Lambda<func>>(call).Compile(); // 返回无参委托
var result = func(); // 直接调用,零反射开销
</func>
C# 12 拦截器(Interceptors)能不能替代表达式树
不能混用。拦截器是编译期静态替换,只适用于已知签名、可提前确定调用目标的场景;而表达式树是运行时构建委托,适合配置驱动或插件化逻辑。
- 拦截器要求被拦截方法标记
[InterceptsLocation(...)],且调用方必须用partial方法语法,实际项目中改造成本高 - 它无法处理
dynamic或未知类型的反射调用 —— 拦截器根本看不到这些运行时才决定的目标 - 如果你正在做 ORM 的属性赋值或 DTO 映射,拦截器不适用;但日志埋点、参数校验这类固定增强,它能把延迟从 800μs 压到 1.2μs
真正容易被忽略的是:缓存策略本身可能成为瓶颈。比如用 ConcurrentDictionary 存委托没问题,但如果键拼接用了字符串插值($"{t}.{m}"),高频初始化阶段会大量触发临时字符串分配 —— 这个开销有时比反射还隐蔽。










