Why is Task<T> not co-variant?

10.4k views Asked by At
class ResultBase {}
class Result : ResultBase {}

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

The compiler tells me that it cannot implicitly convert Task<Result> to Task<ResultBase>. Can someone explain why this is? I would have expected co-variance to enable me to write the code in this way.

4

There are 4 answers

3
tacos_tacos_tacos On BEST ANSWER

According to someone who may be in the know...

The justification is that the advantage of covariance is outweighed by the disadvantage of clutter (i.e. everyone would have to make a decision about whether to use Task or ITask in every single place in their code).

It sounds to me like there is not a very compelling motivation either way. ITask<out T> would require a lot of new overloads, probably quite a bit under the hood (I cannot attest to how the actual base class is implemented or how special it is compared to a naive implementation) but way more in the form of these linq-like extension methods.

Somebody else made a good point - the time would be better spent making classes covariant and contravariant. I don't know how hard that would be, but that sounds like a better use of time to me.

On the other hand, somebody mentioned that it would be very cool to have a real yield return like feature available in an async method. I mean, without sleight of hand.

2
Oleg Bevz On

In my case I didn't know Task generic argument in compile time and had to work with System.Threading.Tasks.Task base class. Here is my solution created from the example above, perhaps will help someone.

    [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);
    }
0
ssmith On

I've had success with the MorseCode.ITask NuGet package. At this point it's pretty stable (no updates in a few years) but it was trivial to install and the only thing you needed to do to translate from an ITask to a Task was call .AsTask() (and the reverse extension method also ships with the package).

9
Sergio0694 On

I realize I'm late to the party, but here's an extension method I've been using to account for this missing feature:

/// <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;
}

This way you can just do:

class ResultBase {}
class Result : ResultBase {}

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

Task<ResultBase> GetResultBaseAsync() 
{
    return GetResultAsync().AsTask<Result, ResultBase>();
}