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

C#泛型接口转换报错:无法将IEventListener<TEvent>转为IEventListener<EventBase>

C#泛型逆变接口转换失败的原因与修复方案

问题原因:逆变方向理解错误

你标记了in关键字的IEventListener<TEvent>确实是逆变接口,但你搞反了它的合法转换方向:

  • 逆变的规则是:如果TDerived是TBase的派生类,那么IEventListener<TBase>可以安全转换为IEventListener<TDerived>(因为接口只接受TEvent作为输入,基类的输入处理逻辑可以兼容派生类)。
  • 你现在试图将IEventListener<TEvent>(TEvent是EventBase的子类)转换为IEventListener<EventBase>,这是逆变的反向操作,完全不符合逆变的设计逻辑,因此编译报错,强制转换会触发运行时异常。

你的字典_subscriptions定义为Dictionary<Type, List<IEventListener<EventBase>>>,但要存入的是IEventListener<TEvent>实例,当TEvent是EventBase的派生类时,这个转换在逆变规则下是不允许的。

修复方案

方案1:新增非泛型基接口(推荐)

添加一个非泛型的IEventListener接口作为泛型接口的基类,让字典存储统一的非泛型实例,既解决存储问题,又保留类型安全:

namespace Game {
    public static class EventSystem {
        public class EventBase { }
        
        // 新增非泛型监听器基接口
        public interface IEventListener { }
        
        public interface IEventListener<in TEvent> : IEventListener where TEvent : EventBase {
            void OnEvent(TEvent ev);
        }
        
        // 字典值类型改为List<IEventListener>
        private static Dictionary<Type, List<IEventListener>> _subscriptions = new();
        
        public static void AddListener<TEvent>(IEventListener<TEvent> listener) where TEvent : EventBase {
            if (!_subscriptions.ContainsKey(typeof(TEvent))) {
                _subscriptions.Add(typeof(TEvent), new());
            }
            var list = _subscriptions[typeof(TEvent)];
            list.Add(listener);
        }
        
        public static void RemoveListener<TEvent>(IEventListener<TEvent> listener) where TEvent : EventBase {
            if (_subscriptions.ContainsKey(typeof(TEvent))) {
                var list = _subscriptions[typeof(TEvent)];
                list.Remove(listener);
            }
        }
        
        // 补充事件触发示例(可选)
        public static void TriggerEvent<TEvent>(TEvent ev) where TEvent : EventBase {
            if (_subscriptions.TryGetValue(typeof(TEvent), out var listeners)) {
                foreach (var listener in listeners.Cast<IEventListener<TEvent>>()) {
                    listener.OnEvent(ev);
                }
            }
        }
    }
}

方案2:使用object存储(类型安全稍弱)

如果不想新增接口,也可以将字典值类型改为List<object>,利用引用相等性完成添加/移除操作,触发事件时再转换为对应的泛型接口:

namespace Game {
    public static class EventSystem {
        public class EventBase { }
        public interface IEventListener<in TEvent> where TEvent : EventBase {
            void OnEvent(TEvent ev);
        }
        
        private static Dictionary<Type, List<object>> _subscriptions = new();
        
        public static void AddListener<TEvent>(IEventListener<TEvent> listener) where TEvent : EventBase {
            if (!_subscriptions.ContainsKey(typeof(TEvent))) {
                _subscriptions.Add(typeof(TEvent), new());
            }
            var list = _subscriptions[typeof(TEvent)];
            list.Add(listener);
        }
        
        public static void RemoveListener<TEvent>(IEventListener<TEvent> listener) where TEvent : EventBase {
            if (_subscriptions.ContainsKey(typeof(TEvent))) {
                var list = _subscriptions[typeof(TEvent)];
                list.Remove(listener);
            }
        }
        
        public static void TriggerEvent<TEvent>(TEvent ev) where TEvent : EventBase {
            if (_subscriptions.TryGetValue(typeof(TEvent), out var listeners)) {
                foreach (var listener in listeners.Cast<IEventListener<TEvent>>()) {
                    listener.OnEvent(ev);
                }
            }
        }
    }
}

总结

核心错误是对逆变方向的误解:逆变允许基类泛型接口向派生类泛型接口转换,而非你尝试的反向转换。通过新增非泛型基接口的方案,既能解决存储兼容问题,又能保证类型安全,是这类事件系统的标准实现方式。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 22:22:51