如何在泛型逆变IHandler接口上正确调用Invoke方法?
这个问题我之前也碰到过,核心是泛型接口的逆变特性和dynamic绑定的细节问题,咱们一步步拆解解决:
先说说两次尝试失败的原因
第一次强制转换失败
你的IHandler<in T>是逆变泛型接口,逆变的规则是:如果Derived继承自Base,你可以把IHandler<Base>转换为IHandler<Derived>,但反过来不行。你这里试图把IHandler<ChatEvent>(泛型参数是子类ChatEvent)强转为IHandler<IEvent>(泛型参数是基类IEvent),这是协变的方向,逆变不支持这种转换,所以抛出InvalidCastException。
第二次dynamic绑定失败
虽然tl的实际类型是MyHandler<ChatEvent>,ev的实际类型也是ChatEvent,但你传入的ev是IEvent类型的变量。dynamic会按照编译时类型(也就是IEvent)去匹配Invoke方法,而MyHandler<ChatEvent>.Invoke只接受ChatEvent类型的参数,所以运行时绑定器找不到匹配的方法,抛出RuntimeBinderException。
推荐解决方案(按优先级排序)
方案一:给IHandler添加非泛型Invoke方法(最推荐,类型安全+高性能)
修改基础接口,新增一个非泛型的Invoke方法,让泛型子类实现它:
interface IHandler { Type Type { get; } // 新增非泛型Invoke方法 void Invoke(IEvent ev); } interface IHandler<in T> : IHandler where T : IEvent { void Invoke(T ev); } // 实现类中显式实现非泛型接口逻辑 class MyHandler<T> : IHandler<T> where T : IEvent { public Type Type => typeof(T); // 泛型Invoke的业务逻辑 public void Invoke(T ev) { // 你的事件处理代码 } // 非泛型Invoke的适配逻辑(因为有Type断言,转换绝对安全) void IHandler.Invoke(IEvent ev) { Invoke((T)ev); } }
然后你的调用代码就可以直接调用非泛型方法:
public void Invoke(IEvent ev) { ImmutableArray<IHandler> selected; lock (_listeners) { selected = GetHandlersFor(ev.GetType()).ToImmutableArray(); } foreach (var tl in selected) { Debug.Assert(tl.Type.IsInstanceOfType(ev)); tl.Invoke(ev); // 直接调用,类型安全且无额外开销 } }
这个方案是最优解,既保证了类型安全,又没有反射或dynamic带来的性能损耗,代码可读性也最高。
方案二:用反射调用泛型Invoke方法
如果不想修改原有接口,可以用反射来获取并调用目标方法:
public void Invoke(IEvent ev) { ImmutableArray<IHandler> selected; lock (_listeners) { selected = GetHandlersFor(ev.GetType()).ToImmutableArray(); } foreach (var tl in selected) { Debug.Assert(tl.Type.IsInstanceOfType(ev)); // 获取泛型Invoke方法并调用 var invokeMethod = tl.GetType().GetMethod(nameof(IHandler<IEvent>.Invoke)); invokeMethod?.Invoke(tl, new[] { ev }); } }
注意:反射有一定性能开销,如果是高频调用场景,建议提前缓存每个IHandler类型的Invoke方法,减少反射次数。
方案三:修正dynamic的调用方式
如果坚持用dynamic,需要先把ev转换为tl.Type对应的类型,让dynamic能正确匹配方法:
public void Invoke(IEvent ev) { ImmutableArray<IHandler> selected; lock (_listeners) { selected = GetHandlersFor(ev.GetType()).ToImmutableArray(); } foreach (var tl in selected) { Debug.Assert(tl.Type.IsInstanceOfType(ev)); // 把ev转换为处理器对应的事件类型 dynamic typedEv = Convert.ChangeType(ev, tl.Type); ((dynamic)tl).Invoke(typedEv); } }
这个方案依赖Convert.ChangeType,你的Debug.Assert已经保证了类型兼容,所以转换是安全的,但dynamic仍有一定性能开销,可读性也不如方案一。
内容的提问来源于stack exchange,提问作者snowflake

