泛型协变理解困惑:C#泛型类入队编译错误排查
问题描述
我在尝试将两个不同泛型参数的FetchLinesWorker实例加入同一队列时遇到编译错误:
var transactionsWorker = new FetchLinesWorker<TransactionLine, UnsoldLine>(new UnsoldLineWrapper(), new FilteredTransactionLines()); var ordersWorker = new FetchLinesWorker<OrderLineDeliveryGood, UnsoldLine>(new UnsoldDeliveryLineWrapper(), new FilteredOrderLines()); var workerQueue = new Queue<FetchLinesWorker<object, UnsoldLine>>(); workerQueue.Enqueue(transactionsWorker); // 编译错误:无法转换类型 workerQueue.Enqueue(ordersWorker); // 同样的编译错误 new WorkerForm(workerQueue, () => whenFetchingFinished(workerQueue.SelectMany(w => w.Result))).StartJob();
编译器提示无法将FetchLinesWorker<XXXLine, UnsoldLine>转换为FetchLinesWorker<object, UnsoldLine>。我有Java背景,原以为C#支持泛型协变,而且Java中可以用Queue<FetchLinesWorker<?, UnsoldLine>>来实现类似场景,请问C#中为什么会报错?有没有等效的写法?
补充上下文代码:
public interface IWrapperLine<In, Out> { Out Convert(In input); } public class FetchLinesWorker<In, Out> : JobWorker { private readonly IQueryable<In> _inputs; private readonly IWrapperLine<In, Out> _converter; public FetchLinesWorker(IWrapperLine<In, Out> converter, IQueryable<In> inputs) : base(0, TranslationsHelper.GetEntityProgressMessage(typeof(In))) { _inputs = inputs; _converter = converter; DoWork += SoldStatisticLinesWorker_DoWork; } private void SoldStatisticLinesWorker_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e) { Result.Clear(); ProgressMax = _inputs.Count(); foreach (var input in _inputs) { Result.Add(_converter.Convert(input)); DoProgress(); } } public List<Out> Result { get; } = new List<Out>(); } public class FilteredTransactionLines : IQueryable<TransactionLine> { /* 实现代码 */ } public class FilteredOrderLines : IQueryable<OrderLineDeliveryGood> { /* 实现代码 */ } public class WorkerForm { public WorkerForm(Queue<JobWorker> workers, Action finishedAction) { /* 实现代码 */ } }
注:JobWorker继承自BackgroundWorker,无法修改该类。
错误原因分析
你遇到的核心问题是C#的泛型协变/逆变仅支持泛型接口和泛型委托,泛型类不支持协变或逆变。
在Java中,FetchLinesWorker<?, UnsoldLine>的通配符?可以接受任意类型的第一个泛型参数,但C#的泛型类没有类似的通配符语法。更关键的是,即使你认为TransactionLine可以向上转型为object,FetchLinesWorker<TransactionLine, UnsoldLine>也不能直接转换为FetchLinesWorker<object, UnsoldLine>——因为FetchLinesWorker是类,C#不允许泛型类的协变转换,哪怕类型参数是兼容的。
另外,即使你想让In参数支持协变,也需要在接口上标记out关键字(协变),但类无法做到这一点。
C#中的等效解决方案
要实现类似Java通配符的效果,我们可以通过定义协变泛型接口来封装你需要的公共行为(比如获取Result),具体步骤如下:
1. 定义协变接口
创建一个只暴露你需要的公共成员的协变接口,这里我们只需要获取Result,所以接口可以这样定义:
// 使用out关键字标记Out为协变类型参数 public interface IFetchLinesWorker<out Out> { List<Out> Result { get; } }
2. 修改FetchLinesWorker实现接口
让FetchLinesWorker实现这个新接口:
public class FetchLinesWorker<In, Out> : JobWorker, IFetchLinesWorker<Out> { // 原有的构造函数、DoWork方法、Result属性保持不变 }
3. 调整队列声明和使用
现在你有两种选择,取决于你是否需要兼容WorkerForm的参数要求:
方案A:直接使用协变接口队列(如果WorkerForm允许修改参数类型)
如果可以修改WorkerForm的构造函数参数类型,直接声明队列:
var workerQueue = new Queue<IFetchLinesWorker<UnsoldLine>>(); workerQueue.Enqueue(transactionsWorker); workerQueue.Enqueue(ordersWorker); // 假设WorkerForm可以接受Queue<IFetchLinesWorker<UnsoldLine>> new WorkerForm(workerQueue, () => whenFetchingFinished(workerQueue.SelectMany(w => w.Result))).StartJob();
方案B:使用JobWorker队列(兼容现有WorkerForm)
因为WorkerForm的构造函数接受Queue<JobWorker>,我们可以直接用这个类型的队列,然后在获取Result时通过OfType筛选并转换为协变接口:
var workerQueue = new Queue<JobWorker>(); workerQueue.Enqueue(transactionsWorker); workerQueue.Enqueue(ordersWorker); new WorkerForm(workerQueue, () => whenFetchingFinished(workerQueue.OfType<IFetchLinesWorker<UnsoldLine>>().SelectMany(w => w.Result)) ).StartJob();
额外说明
- 协变接口的
out关键字表示:如果A是B的子类,那么IFetchLinesWorker<A>可以转换为IFetchLinesWorker<B>,这正好符合我们的需求(所有Worker的Out都是UnsoldLine)。 - 如果你不需要协变,也可以定义一个非泛型接口(比如
IFetchLinesWorker),里面包含IEnumerable<object> Result { get; },但这样需要在使用时强制转换,不如协变接口类型安全。
内容的提问来源于stack exchange,提问作者Spotted

