为啥 Task<T> 不是协变的?

Posted

技术标签:

【中文标题】为啥 Task<T> 不是协变的?【英文标题】:Why is Task<T> not co-variant?为什么 Task<T> 不是协变的? 【发布时间】:2015-09-08 21:50:36 【问题描述】:
class ResultBase 
class Result : ResultBase 

Task<ResultBase> GetResult() 
    return Task.FromResult(new Result());

编译器告诉我它不能将Task&lt;Result&gt; 隐式转换为Task&lt;ResultBase&gt;。有人可以解释这是为什么吗?我本来希望协方差能够让我以这种方式编写代码。

【问题讨论】:

接口只能是协变或逆变的。类总是不变的。阅读更多:***.com/questions/13107071/… 类在 C# 中是不变的。 来自this answer 似乎有人为此写了a covariant ITask<T> wrapper。也可以在a suggestion to implement it here 上投票。 在此示例中,您可以显式提供类型参数:Task.FromResult&lt;ResultBase&gt;(new Result())。它会编译。但是是的,Task 是不变的,这很糟糕。 【参考方案1】:

我在MorseCode.ITask NuGet package 上取得了成功。此时它非常稳定(几年内没有更新),但安装起来很简单,从 ITask 转换为 Task 所需要做的唯一事情就是调用 .AsTask() (并且反向扩展方法也附带包)。

【讨论】:

【参考方案2】:

在我的情况下,我在编译时不知道 Task 通用参数,并且必须使用 System.Threading.Tasks.Task 基类。这是我根据上面的示例创建的解决方案,也许会对某人有所帮助。

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static async Task<T> AsTask<T>(this Task task)
    
        var taskType = task.GetType();
        await task;
        return (T)taskType.GetProperty("Result").GetValue(task);
    

【讨论】:

如果您在编译时不知道类型,那么您根本不应该使用泛型。此外,由于您正在使用反射,此函数的执行速度会非常慢 - 如果 Task 在运行时不是 Task&lt;T&gt;,它将失败。 @Dai,是的,它只是一个原型,应该有一个类型检查【参考方案3】:

我意识到我迟到了,但这是我一直在使用的一种扩展方法来解决这个缺失的功能:

/// <summary>
/// Casts the result type of the input task as if it were covariant
/// </summary>
/// <typeparam name="T">The original result type of the task</typeparam>
/// <typeparam name="TResult">The covariant type to return</typeparam>
/// <param name="task">The target task to cast</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static async Task<TResult> AsTask<T, TResult>(this Task<T> task) 
    where T : TResult 
    where TResult : class

    return await task;

这样你就可以做到:

class ResultBase 
class Result : ResultBase 

Task<Result> GetResultAsync() => ...; // Some async code that returns Result

Task<ResultBase> GetResultBaseAsync() 

    return GetResultAsync().AsTask<Result, ResultBase>();

【讨论】:

来自哪个命名空间? @BennyM Task&lt;T&gt;ContinueWith 方法在System.Threading.Tasks 命名空间中,而MethodImpl 属性在System.Runtime.CompilerServices 中。 一些额外的建议:如果你只是想低调,就像在例子中一样,这也可以:Task.FromResult&lt;ResultBase&gt;(new Result()); 实际上,后一条评论应该是公认的答案,因为整个问题不是关于协方差,而是框架已经支持的情况。 此代码正在创建额外的异步状态机,这将对性能产生影响。这仍然不是理想的解决方案。我想知道是否有理想的。【参考方案4】:

根据someone who may be in the know...

理由是协方差的优势超过了 杂乱的缺点(即每个人都必须做一个 决定是否在每个单独的任务中使用 Task 或 ITask 放在他们的代码中)。

在我看来,这两种方式都没有非常令人信服的动机。 ITask&lt;out T&gt; 需要大量新的重载,可能在幕后相当多(我无法证明实际的基类是如何实现的,或者它与幼稚的实现相比有多么特别),但更多的是这些@ 987654324@-like 扩展方法。

其他人提出了一个很好的观点 - 最好把时间花在使 classes 协变和逆变。我不知道这会有多难,但对我来说,这听起来像是更好地利用时间。

另一方面,有人提到在async 方法中提供真正的类似yield return 的功能会非常酷。我的意思是,没有花招。

【讨论】:

async, await 依赖于合适的GetAwaiter 方法的存在,因此它已经与Task 类解耦。 其实TaskTask&lt;&gt;IAsyncEnumerable&lt;&gt;IAsyncEnumerator&lt;&gt;是在c#中制作asyncawait代码所必需的。仅具有GetAwaiter 方法的类是不够的。至少,编译器是这么说的。 @FranciscoNeto 这根本不是真的。 async/await 仅依赖于 GetAwaiter 方法的存在,该方法返回一个对象,该对象具有 IsCompletedOnCompletedGetResult 具有适当签名的成员。因此,可等待对象的自定义实现是可能的。 See here

以上是关于为啥 Task<T> 不是协变的?的主要内容,如果未能解决你的问题,请参考以下文章

直观地解释为啥`List`是协变的,而`Array`是不变的?

在 C# 4.0 中,为啥方法中的 out 参数不能是协变的?

.NET 4 中的 IDictionary<TKey, TValue> 不是协变的

为啥数组是不变的,而列表是协变的?

泛型之逆变和协变总结

编写高质量代码改善C#程序的157个建议——建议44:理解委托中的协变