C#自定义委托:如何限定首参数为ClientMetadata并支持多参数回调
问题与解决方案
问题概述
需要实现WebSocket客户端回调的注册机制,要求满足:
- IDE能强制识别回调的第一个参数必须为
ClientMetadata类型; - 回调支持任意数量、任意类型的后续参数;
- 注册方式需符合直观语法:
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) => { // 逻辑 });
注意事项
- 两种方案都需要处理参数类型转换异常,建议在
DynamicInvoke或转换逻辑中添加try-catch; - 若使用方案一,字典类型需为
Dictionary<string, Delegate>以兼容所有泛型委托; - 若业务需要支持超过16个参数的回调,方案二更适合。
内容的提问来源于stack exchange,提问作者Warstek
相关产品推荐
相关产品推荐

