You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何正确使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 11:27:02