如何创建含泛型列表的字典?事件总线类型适配问题求助
问题分析
你遇到的cannot convert from Subscription<T> to Subscription<IEventCallback>错误,核心原因是泛型默认不支持协变。即便T继承自IEventCallback,Subscription<T>与Subscription<IEventCallback>仍属于不同的封闭泛型类型,编译器不会自动允许转换。
你用List<Object>的方案虽能运行,但存在明显缺陷:类型不安全、依赖强制转换、无法在编译期排查错误,且原Unsubscribe方法存在逻辑错误(_subscribers[type].Remove(type)应为移除action而非type)。
优雅解决方案:利用泛型协变
通过定义协变泛型接口,可在保持类型安全的前提下解决转换问题。
步骤1:定义协变订阅接口
创建带out关键字的协变接口,标记泛型参数为协变类型,这样当T是IEventCallback的子类时,ISubscription<T>可隐式转换为ISubscription<IEventCallback>:
public interface IEventCallback { } // 协变接口,out关键字允许泛型类型向上转换 public interface ISubscription<out T> where T : IEventCallback { void Publish(T callback); }
步骤2:实现Subscription类
让Subscription<T>实现上述协变接口:
public class Subscription<T> : ISubscription<T> where T : IEventCallback { private readonly Action<T> _action; public Subscription(Action<T> action) { _action = action ?? throw new ArgumentNullException(nameof(action)); } public void Publish(T callback) { _action.Invoke(callback); } }
步骤3:重构EventBus类
将Dictionary类型改为Dictionary<Type, List<ISubscription<IEventCallback>>>,即可安全添加Subscription<T>实例:
public class EventBus { private readonly Dictionary<Type, List<ISubscription<IEventCallback>>> _subscribers = new(); // 保留原有单例实现 public static EventBus Bus => INSTANCE.Value; private static readonly Lazy<EventBus> INSTANCE = new(() => new EventBus()); public void Subscribe<T>(Action<T> action) where T : IEventCallback { ArgumentNullException.ThrowIfNull(action); var eventType = typeof(T); if (!_subscribers.ContainsKey(eventType)) { _subscribers[eventType] = new List<ISubscription<IEventCallback>>(); } // 协变支持隐式转换,无编译错误 _subscribers[eventType].Add(new Subscription<T>(action)); } public void Unsubscribe<T>(Action<T> action) where T : IEventCallback { ArgumentNullException.ThrowIfNull(action); var eventType = typeof(T); if (_subscribers.TryGetValue(eventType, out var subscriptions)) { // 通过反射查找对应Action的Subscription实例并移除 var toRemove = subscriptions.OfType<Subscription<T>>() .FirstOrDefault(s => s.GetType().GetField("_action", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(s) == action); if (toRemove != null) { subscriptions.Remove(toRemove); } } } public void Publish<T>(T eventData) where T : IEventCallback { ArgumentNullException.ThrowIfNull(eventData); var eventType = typeof(T); if (_subscribers.TryGetValue(eventType, out var subscriptions)) { // 转换为具体泛型类型后触发事件 foreach (var subscription in subscriptions.Cast<ISubscription<T>>()) { subscription.Publish(eventData); } } } }
关键说明
- 协变接口:
out T使ISubscription<T>可向上转换为ISubscription<IEventCallback>,彻底解决类型转换错误。 - 类型安全:全程无需强制转换(Unsubscribe的反射仅为保持封装,若允许暴露
_action可简化逻辑)。 - 修复Unsubscribe错误:原代码中移除type的逻辑错误已修正为查找对应Subscription实例。
简化方案:直接存储包装后的Action
若无需Subscription类封装,可直接将Action<T>包装为Action<IEventCallback>,Dictionary存储List<Action<IEventCallback>>:
public class EventBus { private readonly Dictionary<Type, List<(Action<T> Original, Action<IEventCallback> Wrapped)>> _subscribers = new(); public static EventBus Bus => INSTANCE.Value; private static readonly Lazy<EventBus> INSTANCE = new(() => new EventBus()); public void Subscribe<T>(Action<T> action) where T : IEventCallback { ArgumentNullException.ThrowIfNull(action); var eventType = typeof(T); if (!_subscribers.ContainsKey(eventType)) { _subscribers[eventType] = new List<(Action<T>, Action<IEventCallback>)>(); } // 包装Action<T>为Action<IEventCallback> Action<IEventCallback> wrapped = callback => action((T)callback); _subscribers[eventType].Add((action, wrapped)); } public void Publish<T>(T eventData) where T : IEventCallback { ArgumentNullException.ThrowIfNull(eventData); var eventType = typeof(T); if (_subscribers.TryGetValue(eventType, out var actions)) { foreach (var (_, wrapped) in actions) { wrapped.Invoke(eventData); } } } public void Unsubscribe<T>(Action<T> action) where T : IEventCallback { ArgumentNullException.ThrowIfNull(action); var eventType = typeof(T); if (_subscribers.TryGetValue(eventType, out var actions)) { var toRemove = actions.FirstOrDefault(pair => pair.Original == action); if (toRemove.Original != null) { actions.Remove(toRemove); } } } }
此方案更简洁,通过存储原始Action与包装Action的元组,解决了Unsubscribe的查找问题。
内容的提问来源于stack exchange,提问作者Vlad Panfilov

