如何正确使用C# CallSite与CallSiteBinder实现带缓存的动态方法调用
自定义C# DSL解释器中CallSite缓存的正确使用问题
问题背景
我正在用纯C#编写自定义DSL解释器,希望正确利用内置的调用站点缓存功能。目前演示程序仅在类型未发生变化时能正常运行,切换类型就会抛出异常,想确认是否正确使用了Binder、Target或CallSite,并寻求完整实现示例。
演示程序代码
using System.Collections.ObjectModel; using System.Linq.Expressions; using System.Runtime.CompilerServices; public class Program2 { public static void Main() { // 创建CallSite var callSite = CallSite<Func<CallSite, object, object, object>>.Create(new AdditionBinder()); // 执行动态加法 var add = callSite.Target; var result1 = add(callSite, 5, 10); // 首次调用int,int组合,Bind被调用并编译int,int版本函数 var result2 = add(callSite, 10, 10); // 复用已编译的int,int函数,Bind未被调用,正常运行 var result3 = add(callSite, "strA", "str1"); // 未调用Bind就抛出异常:"Unable to cast object of type 'System.String' to type 'System.Int32'" var result4 = add(callSite, "strB", "str2"); } }
自定义AdditionBinder实现
public class AdditionBinder : CallSiteBinder { public override Expression Bind(object[] args, ReadOnlyCollection<ParameterExpression> parameters, LabelTarget returnLabel) { // 获取要相加的两个参数 var left = parameters[0]; var right = parameters[1]; // 后续会优化类型匹配逻辑,当前仅实现基础类型组合 Expression addition = null; if (args[0] is int && args[1] is int) { // int类型加法 addition = Expression.Convert( Expression.Add( Expression.Convert(left, typeof(int)), Expression.Convert(right, typeof(int)) ), typeof(object) ); } else if (args[0] is string && args[1] is string) { // string类型拼接 addition = Expression.Convert( Expression.Add( Expression.Convert(left, typeof(string)), Expression.Convert(right, typeof(string)), typeof(string).GetMethod("Concat", new[] { typeof(string), typeof(string) }) ), typeof(object) ); } else throw new InvalidOperationException("No add for T + T"); // 返回加法结果 return Expression.Return(returnLabel, addition); } }
更新问题:共享Binder的多CallSite实例异常
尝试用同一个Binder创建两个独立CallSite实例,出现类型混淆异常:
var sharedBinder = new AdditionBinder(); var callSite = CallSite<Func<CallSite, object, object, object>>.Create(sharedBinder); var callSite2 = CallSite<Func<CallSite, object, object, object>>.Create(sharedBinder); _ = callSite2.Target(callSite2, "str", "str"); // 正常运行 _ = callSite.Target(callSite, 5, 10); // 抛出异常,仿佛两个CallSite共享了缓存的Target
预期的DSL解释器工作模式
# 假设myAdd定义对应一个CallSiteBinder实例,缓存所有类型特化的编译委托 myAdd = x,y => x+y # 给所有整数加42 # 假设这里的myAdd调用对应一个CallSiteCache实例,L1缓存myAdd(int,int) # 遇到string类型时不会抛出异常,复用共享CallSiteBinder的L2缓存委托 exampleAlgoInts = intArr => intArr.Select(i => myAdd(i,42)) # 给所有名称添加前缀"Hi " # 假设这里的myAdd调用对应另一个CallSiteCache实例,L1缓存myAdd(string,string) # 遇到double、int等类型时不会抛出异常,复用共享CallSiteBinder的L2缓存委托 exampleAlgoStrings = strArr => strArr.Select(name => myAdd("Hi ",name))
我清楚这是核心团队不建议外部开发者使用的内部机制,但希望理解其工作原理以优化解释器性能。
问题根源与解决方案
1. 核心错误:缓存CallSite.Target导致无法更新
你直接将callSite.Target缓存到add变量,但CallSite的Target会在Bind成功后自动更新为特化委托。缓存旧的Target会导致类型切换时,仍然调用之前的特化委托(比如int版本),自然抛出类型转换异常。
正确做法:每次调用都使用callSite.Target,不要缓存它:
// 错误写法:缓存Target,无法获取更新后的特化委托 // var add = callSite.Target; // var result1 = add(callSite,5,10); // 正确写法:每次调用使用当前的callSite.Target var result1 = callSite.Target(callSite, 5, 10); var result2 = callSite.Target(callSite, 10, 10); var result3 = callSite.Target(callSite, "strA", "str1"); // 触发Bind,编译string版本 var result4 = callSite.Target(callSite, "strB", "str2"); // 复用string版本
2. 参数顺序错误:混淆CallSite与业务参数
你之前的Binder中错误地将parameters[0]和parameters[1]当作左右参数,但CallSite的委托签名是Func<CallSite, object, object, object>,参数顺序为:
parameters[0]:CallSite实例本身parameters[1]:左业务参数parameters[2]:右业务参数
这是导致类型匹配逻辑错误的关键原因之一。
3. 修正后的AdditionBinder实现
修改Binder,生成包含类型检查的分支表达式,同时重写CacheIdentity确保缓存区分:
public class AdditionBinder : CallSiteBinder { // 无状态Binder返回自身作为缓存标识,确保不同类型组合的缓存独立性 public override object CacheIdentity => this; public override Expression Bind(object[] args, ReadOnlyCollection<ParameterExpression> parameters, LabelTarget returnLabel) { var callSiteParam = parameters[0]; var leftParam = parameters[1]; var rightParam = parameters[2]; var intType = typeof(int); var stringType = typeof(string); // int类型处理分支 var intBranch = Expression.IfThenElse( Expression.AndAlso( Expression.TypeIs(leftParam, intType), Expression.TypeIs(rightParam, intType) ), Expression.Return(returnLabel, Expression.Convert( Expression.Add( Expression.Convert(leftParam, intType), Expression.Convert(rightParam, intType) ), typeof(object) )), Expression.Empty() ); // string类型处理分支 var stringBranch = Expression.IfThenElse( Expression.AndAlso( Expression.TypeIs(leftParam, stringType), Expression.TypeIs(rightParam, stringType) ), Expression.Return(returnLabel, Expression.Convert( Expression.Add( Expression.Convert(leftParam, stringType), Expression.Convert(rightParam, stringType), stringType.GetMethod("Concat", new[] { stringType, stringType }) ), typeof(object) )), Expression.Empty() ); // 默认异常分支 var defaultBranch = Expression.Throw( Expression.Constant(new InvalidOperationException("No add for T + T")), typeof(object) ); // 组合所有分支 return Expression.Block(intBranch, stringBranch, defaultBranch); } }
4. 共享Binder的多CallSite正确使用
修正后,同一个Binder创建的多个CallSite会各自维护独立缓存,不会出现类型混淆:
var sharedBinder = new AdditionBinder(); var callSite = CallSite<Func<CallSite, object, object, object>>.Create(sharedBinder); var callSite2 = CallSite<Func<CallSite, object, object, object>>.Create(sharedBinder); _ = callSite2.Target(callSite2, "str", "str"); // callSite2缓存string版本逻辑 _ = callSite.Target(callSite, 5, 10); // callSite缓存int版本逻辑,互不干扰
总结
- 禁止缓存
callSite.Target,每次调用直接使用callSite.Target,确保获取最新的特化委托 - 严格遵循CallSite委托的参数顺序:第一个参数是CallSite实例,后续为业务参数
- Binder需生成多类型分支的表达式,或支持根据参数类型触发重新绑定
- 重写
CacheIdentity确保不同绑定逻辑的缓存独立性
内容的提问来源于stack exchange,提问作者Richard Vondracek
相关产品推荐
相关产品推荐

