为什么Task<T>不支持协变性?

92
class ResultBase {}
class Result : ResultBase {}

Task<ResultBase> GetResult() {
    return Task.FromResult(new Result());
}
编译器告诉我它无法将 Task<Result> 隐式转换为 Task<ResultBase>。有人能解释一下为什么吗?我本来以为协变会让我以这种方式编写代码。

3
接口只能是协变的或者是逆变的,而类始终是不变的。更多信息请参阅:https://dev59.com/dGcs5IYBdhLWcg3wHwfU - user2160375
C# 中的类是不变的。 - Lee
7
根据这个答案,似乎有人为此编写了一个协变的 ITask<T> 包装器。此外,你可以在这里提出建议实现它 - Matthew Watson
3
在这个例子中,你可以显式地提供类型参数:Task.FromResult<ResultBase>(new Result())。它可以编译通过。但是,是的,Task 是逆变的,这很糟糕。 - ps_ttf
4个回答

36

根据消息灵通的人的说法...

正当性在于,协变性的优势被杂乱无序的缺点所抵消(即每个人都必须决定是否在代码的每个地方使用Task还是ITask)。

对我来说,似乎没有非常令人信服的动机。 ITask<out T>将需要很多新的重载,可能会在底层有相当大的影响(我不能证明实际基类的实现方式或者它与天真实现相比有多特殊),但在这些linq类似的扩展方法中需要更多的工作。

其他人提出了一个好观点 - 把时间花在使class具有协变性和逆变性上会更好。我不知道那会有多难,但对我来说,那听起来是更好的时间利用方式。

另一方面,有人提到在async方法中添加一个真正的yield return特性会非常酷。我的意思是,不需要任何巧妙的手法。


7
asyncawait依赖于存在一个适当的GetAwaiter方法,因此它已经与Task类解耦。 - Lee
实际上,为了使C#中的异步等待代码正常运行,需要使用TaskTask<>IAsyncEnumerable<>IAsyncEnumerator<>。仅具有GetAwaiter方法的类是不够的。至少编译器是这么说的。 - Francisco Neto
5
@FranciscoNeto 这并不是事实。 async/await 只依赖于存在一个返回一个具有适当签名的 IsCompletedOnCompletedGetResult 成员的对象的 GetAwaiter 方法。因此,可自定义等待方法的实现。请参见此处 - Luke Caswell Samuel

20

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

/// <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
10
一些建议: 如果你只想降级,就像这个例子一样,也可以这样写: Task.FromResult<ResultBase>(new Result()); - Bluuu
2
实际上,后面的评论应该被接受为答案,因为整个问题不是关于协变性,而是一个已经被框架支持的情况。 - Daniel Leiszen
1
这段代码正在创建额外的异步状态机,这将对性能产生影响。但这仍然不是理想的解决方案。我想知道是否有更理想的解决方案。 - Shoter
为什么要使用显式的 where TResult : class - chtenb
显示剩余4条评论

3
我使用过MorseCode.ITask NuGet包并获得了成功。目前它非常稳定(数年没有更新),但安装非常简单,只需调用.AsTask()即可将ITask转换为Task(反向扩展方法也包含在该包中)。请注意保留HTML标签。

-1
在我的情况下,我不知道编译时的任务泛型参数,只能使用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<T>,则会失败。 - Dai
@Dai,是的,这只是一个原型,应该有类型检查。 - Oleg Bevz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接