89
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.

Frédéric Hamidi
  • 258,201
  • 41
  • 486
  • 479
chosenbreed37
  • 1,292
  • 9
  • 10
  • 3
    Infterfaces can only be covariant or contravariant. Class always are invariant. Read more on: http://stackoverflow.com/questions/13107071/why-classes-that-implement-variant-interfaces-remain-invariant –  Jun 23 '15 at 07:58
  • Classes are invariant in C#. – Lee Jun 23 '15 at 07:58
  • 7
    From [this answer](http://stackoverflow.com/questions/12204755/can-should-tasktresult-be-wrapped-in-a-c-sharp-5-0-awaitable-which-is-covarian) it appears that someone has written [a covariant ITask wrapper](https://github.com/jam40jeff/ITask) for it. Also one can vote on [a suggestion to implement it here](https://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/5754247-make-task-t-implement-covariant-interface-itask-o). – Matthew Watson Jun 23 '15 at 08:06
  • 3
    In this example you can provide the type parameter explicitly: `Task.FromResult(new Result())`. It will compile. But yes, `Task` is invariant, which is bad. – ps_ttf Jul 16 '16 at 15:13

4 Answers4

35

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.

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
tacos_tacos_tacos
  • 10,277
  • 11
  • 73
  • 126
  • 6
    `async`, `await` relies on the existence of a suitable `GetAwaiter` method so it's already decoupled from the `Task` class. – Lee Jun 23 '15 at 08:00
  • Actually `Task`, `Task<>`, `IAsyncEnumerable<>` or `IAsyncEnumerator<>` is required to make `async` `await` code in c#. A class with the `GetAwaiter` method alone is not enough. At least, this is what the compiler says. – Francisco Neto Nov 25 '19 at 20:25
  • 4
    @FranciscoNeto That's simply not true. `async`/`await` only rely on the existence of a `GetAwaiter` method which returns an object having `IsCompleted`, `OnCompleted` and `GetResult` members with appropriate signatures. Custom implementations of awaitables are therefore possible. [See here](https://codeblog.jonskeet.uk/2011/05/13/eduasync-part-3-the-shape-of-the-async-method-awaitable-boundary/) – Luke Caswell Samuel Mar 07 '20 at 06:10
19

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>();
}
Sergio0694
  • 4,447
  • 3
  • 31
  • 58
  • From what namespace? – BennyM Apr 25 '18 at 12:25
  • @BennyM `Task` and the `ContinueWith` method are in the `System.Threading.Tasks` namespace, while the `MethodImpl` attribute is in `System.Runtime.CompilerServices`. – Sergio0694 Apr 25 '18 at 15:05
  • 9
    Some additional advice: If you just want to downcast, like in the example, this works as well: `Task.FromResult(new Result());` – Bluuu May 17 '18 at 07:12
  • 2
    Actually this latter comment should be the accepted answer since the whole problem is not about covariance but a situation that is an already supported by the framework. – Daniel Leiszen Dec 14 '18 at 23:17
  • Do not use that. ContinueWith should not be used with Async Flow. Especially when you do not define that you need to handle it asynchrounously. – Shoter Nov 21 '19 at 19:18
  • @Shoter that is a very good point, in fact I originally wrote this answer 2 years ago when I had way less experience with the intricacies of async code in C#. I've updated my answer, the code is now equivalent and should also preserve the stack trace correctly when used. The performance should be roughly the same as well: you get an additional async state machine but you get rid of the delegate invocation that was there before. Thanks for pointing this out! – Sergio0694 Nov 21 '19 at 21:25
  • 1
    This code is creating additional async state machine which will have impact on performance. This is still not ideal solution. I wonder if there is ideal one though. – Shoter Nov 24 '19 at 19:14
  • @Shoter I know it adds some overhead due to the additional async state machine (which is why I had originally not done so), but it's the only proper way to do this that I'm aware of. As you pointed out, the previous approach was using `ContinueWith` which is highly discouraged with the async flow, and which also cases the stack traces to basically be lost, or at least partially lost when that method is somewhere in the execution flow. If there's a better way to do this I'd love to learn what that is! Until then, I think this is the best approach for this particular issue. – Sergio0694 Nov 24 '19 at 19:19
  • Why the explicit `where TResult : class`? – chtenb Jul 16 '21 at 14:24
3

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).

ssmith
  • 8,092
  • 6
  • 52
  • 93
-1

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);
    }
Oleg Bevz
  • 139
  • 7
  • If you don't know the type at compile-time then you shouldn't be using generics at all. Also, this function will perform very slowly because you're using reflection - and it will fail if `Task` is not `Task` at runtime. – Dai Jun 08 '21 at 14:40
  • @Dai, yep, it's just a prototype, there should be a type check – Oleg Bevz Jun 09 '21 at 16:01