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

C#自定义委托:如何限定首参数为ClientMetadata并支持多参数回调

问题与解决方案

问题概述

需要实现WebSocket客户端回调的注册机制,要求满足:

  1. IDE能强制识别回调的第一个参数必须为ClientMetadata类型;
  2. 回调支持任意数量、任意类型的后续参数;
  3. 注册方式需符合直观语法:
    RegisterEventHandler("callback1", (ClientMetadata client, string smth1, int smth2) => { /* 逻辑 */ });
    RegisterEventHandler("callback2", (ClientMetadata client, bool smth3) => { /* 逻辑 */ });
    

现有实现中,自定义委托的params关键字无法生效,IDE要求必须传入object[]类型参数,不符合预期。


解决方案

方案一:泛型委托+重载注册方法

通过定义多参数泛型委托,并为每个委托重载RegisterEventHandler方法,让IDE自动推导参数类型,同时强制第一个参数为ClientMetadata。

1. 定义泛型委托

根据业务需求定义覆盖常用参数数量的泛型委托(C#最多支持16个泛型参数,足够大部分场景):

// 无后续参数的回调
public delegate void EventHandlerCallback(ClientMetadata client);
// 1个后续参数的回调
public delegate void EventHandlerCallback<T1>(ClientMetadata client, T1 arg1);
// 2个后续参数的回调
public delegate void EventHandlerCallback<T1, T2>(ClientMetadata client, T1 arg1, T2 arg2);
// 3个后续参数的回调(按需添加更多)
public delegate void EventHandlerCallback<T1, T2, T3>(ClientMetadata client, T1 arg1, T2 arg2, T3 arg3);

2. 重载注册方法

为每个泛型委托实现对应的RegisterEventHandler重载:

// 存储所有回调的字典,类型为Delegate以兼容所有泛型委托
private static Dictionary<string, Delegate> callbacks = new Dictionary<string, Delegate>();

public static void RegisterEventHandler(string identifier, EventHandlerCallback callback)
{
    callbacks.Add(identifier, callback);
}

public static void RegisterEventHandler<T1>(string identifier, EventHandlerCallback<T1> callback)
{
    callbacks.Add(identifier, callback);
}

public static void RegisterEventHandler<T1, T2>(string identifier, EventHandlerCallback<T1, T2> callback)
{
    callbacks.Add(identifier, callback);
}

public static void RegisterEventHandler<T1, T2, T3>(string identifier, EventHandlerCallback<T1, T2, T3> callback)
{
    callbacks.Add(identifier, callback);
}

3. 修正回调调用逻辑

原调用逻辑中DynamicInvoke传递参数的方式错误,需将e.Client作为第一个参数,obj.message的元素逐个作为后续参数传递:

private static void Server_MessageReceive2d(object? sender, MessageReceivedEventArgs e)
{
    string jsonObj = Encoding.UTF8.GetString(e.Data);
    if (string.IsNullOrEmpty(jsonObj)) return;

    WSMessage obj = JsonConvert.DeserializeObject<WSMessage>(jsonObj);
    if (obj == null || obj.message == null) return;

    if (callbacks.TryGetValue(obj.identifier, out var callback))
    {
        MethodInfo mi = callback.GetMethodInfo();
        // 构建参数列表:第一个参数是ClientMetadata,后续是message中的元素
        var paramList = new List<object> { e.Client };
        if (obj.message is IEnumerable<object> messageArgs)
        {
            paramList.AddRange(messageArgs);
        }

        try
        {
            if (obj.callbackId != null && mi.ReturnType != typeof(void))
            {
                var ret = callback.DynamicInvoke(paramList.ToArray());
                if (ret != null)
                {
                    SendMessage(e.Client, obj.callbackId, ret);
                }
                else
                {
                    SendMessage(e.Client, obj.callbackId);
                }
            }
            else
            {
                callback.DynamicInvoke(paramList.ToArray());
            }
        }
        catch (Exception ex)
        {
            // 处理参数类型不匹配、数量不足等异常
            Console.WriteLine($"回调执行失败:{ex.Message}");
        }
    }
}

效果

注册回调时,IDE会自动推导泛型参数,强制第一个参数为ClientMetadata,后续参数类型会被检查,完全符合预期语法:

RegisterEventHandler("callback1", (ClientMetadata client, string smth1, int smth2) => {
    Console.WriteLine($"收到参数:{smth1}, {smth2}");
});
RegisterEventHandler("callback2", (ClientMetadata client, bool smth3) => {
    Console.WriteLine($"收到参数:{smth3}");
});

方案二:自定义委托+隐式转换

通过定义一个基础委托,并添加隐式转换逻辑,让任意符合第一个参数为ClientMetadata的lambda/方法自动转换为该委托。

1. 定义基础委托

public delegate void EventHandlerCallback(ClientMetadata client, params object[] args);

// 存储字典
private static Dictionary<string, EventHandlerCallback> callbacks = new Dictionary<string, EventHandlerCallback>();

2. 添加隐式转换扩展

为不同参数数量的Action委托添加隐式转换到EventHandlerCallback的逻辑:

public static class EventHandlerCallbackConverters
{
    // 无后续参数
    public static implicit operator EventHandlerCallback(Action<ClientMetadata> action)
    {
        return (client, args) => action(client);
    }

    // 1个后续参数
    public static implicit operator EventHandlerCallback<T1>(Action<ClientMetadata, T1> action)
    {
        return (client, args) => 
        {
            if (args.Length < 1)
                throw new ArgumentException("参数数量不足");
            action(client, (T1)args[0]);
        };
    }

    // 2个后续参数
    public static implicit operator EventHandlerCallback<T1, T2>(Action<ClientMetadata, T1, T2> action)
    {
        return (client, args) => 
        {
            if (args.Length < 2)
                throw new ArgumentException("参数数量不足");
            action(client, (T1)args[0], (T2)args[1]);
        };
    }

    // 按需添加更多参数数量的转换
}

3. 简化注册方法

public static void RegisterEventHandler(string identifier, EventHandlerCallback callback)
{
    callbacks.Add(identifier, callback);
}

4. 调用逻辑同方案一

同样需要拆分obj.message的元素作为参数传递,调用逻辑与方案一一致。

效果

注册时无需显式指定泛型,IDE会自动完成隐式转换,同时检查参数类型:

RegisterEventHandler("callback1", (ClientMetadata client, string smth1, int smth2) => {
    // 逻辑
});
RegisterEventHandler("callback2", (ClientMetadata client, bool smth3) => {
    // 逻辑
});

注意事项

  1. 两种方案都需要处理参数类型转换异常,建议在DynamicInvoke或转换逻辑中添加try-catch;
  2. 若使用方案一,字典类型需为Dictionary<string, Delegate>以兼容所有泛型委托;
  3. 若业务需要支持超过16个参数的回调,方案二更适合。

内容的提问来源于stack exchange,提问作者Warstek

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 22:00:58