泛型接口协变问题:游戏任务系统无法添加特定任务的解决方法
为什么会出现类型转换失败?
C# 默认的泛型接口是不变类型,也就是说IGameTask<IBuildTaskPayload>和IGameTask<IPayload>属于完全独立的类型,无法直接互相转换。
你的场景中,IGameTask<T>的UpdateTask方法接收T作为输入参数,属于输入型泛型参数。如果强行允许IGameTask<IBuildTaskPayload>转换为IGameTask<IPayload>,会破坏类型安全:后续若调用该任务的UpdateTask时传入一个普通IPayload实例(而非IBuildTaskPayload),BuildTask的实现逻辑无法处理该类型,必然引发错误。因此C#编译器直接禁止了这种不安全的转换。
正确实现方案
方案一:定义非泛型基础接口(推荐)
如果任务仓库无需知晓具体Payload类型,只需统一管理任务实例,可先创建一个非泛型的IGameTask接口,让泛型接口继承它:
// 非泛型基础接口,存放所有任务的通用成员 public interface IGameTask { TaskStatus CurrentStatus { get; } // 可添加其他通用属性/方法,比如任务ID、取消方法等 } // 泛型任务接口,继承自非泛型接口 public interface IGameTask<in T> : IGameTask where T : IPayload { TaskStatus UpdateTask(T updatePayload); }
随后修改任务仓库的存储容器和添加方法:
// 仓库改为存储非泛型IGameTask public List<IGameTask> Tasks { get; } = new(); internal void AddTask(IGameTask task) { Tasks.Add(task); Broker.Instance.Publish(new TaskAdded(task)); }
这样BuildTask(实现IGameTask<IBuildTaskPayload>)可以直接添加到仓库,因为它自动继承了非泛型的IGameTask接口,完全符合仓库的存储要求。
方案二:利用逆变特性(仅限特定场景)
如果仓库必须保留泛型类型,且调用UpdateTask时只会传入对应具体类型的Payload,可以将IGameTask的泛型参数声明为逆变(使用in关键字):
// 声明逆变泛型参数in T public interface IGameTask<in T> where T : IPayload { TaskStatus UpdateTask(T updatePayload); }
注意:逆变泛型参数只能作为方法的输入参数,不能作为返回值或属性的类型。这种方案下,你无法将IGameTask<IBuildTaskPayload>直接存入List<IGameTask<IPayload>>(逆变允许的是更抽象的IGameTask<IPayload>转换为更具体的IGameTask<IBuildTaskPayload>),因此仅适用于仓库需要处理“接收任意IPayload任务”的场景,大多数游戏任务系统中方案一更实用。
内容的提问来源于stack exchange,提问作者Coreylh

