Why Is Task<T> Not Co-Variant

Why is TaskT not co-variant?

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.

How to adapt TaskT to provide covariant interface without loss of async/await syntax?

Based on the @GuruStron's comment, I found this repo that contains exactly what I was asking about. So, the default builder for Task<T> is System.Runtime.CompilerServices.AsyncTaskMethodBuilder<T>.

The adapter of the default builder can be implemented as the following

public struct JobBuilder<T>
{
private readonly AsyncTaskMethodBuilder<T> _origin;

private JobBuilder(AsyncTaskMethodBuilder<T> origin) =>
_origin = origin;

public IAsyncJob<T> Task => new Job<T>(_origin.Task);

public static Builder<T> Create() =>
new Builder<T>(AsyncTaskMethodBuilder<T>.Create());

public void Start<TStateMachine>(ref TStateMachine stateMachine)
where TStateMachine : IAsyncStateMachine =>
_origin.Start(ref stateMachine);

public void SetStateMachine(IAsyncStateMachine stateMachine) =>
_origin.SetStateMachine(stateMachine);

public void SetResult(T result) =>
_origin.SetResult(result);

public void SetException(Exception exception) =>
_origin.SetException(exception);

public void AwaitOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : INotifyCompletion
where TStateMachine : IAsyncStateMachine =>
_origin.AwaitOnCompleted(ref awaiter, ref stateMachine);

public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine =>
_origin.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
}

Why does the compiler only allow a generic returned task if I `async`/`await` it?

In GetChildClass, you're trying to convert a Task<ChildClass> into Task<BaseClass> - that doesn't work, because Task<T> is invariant (as are all classes; only interfaces and delegates can be generically covariant or contravariant - all arrays support covariance, although in an "interesting" way).

In GetChildClassAsync, you're trying to convert a ChildClass into a BaseClass (which is allowed by normal inheritance and conversions) - and the C# compiler then does the wrapping into a Task<BaseClass>, so that's fine.

I see why it appears to be redundant, and there are potentially more efficient ways that it could be done (it would be nice to be able to create a Task<SomeBaseType> from Task<SomeChildType> using a framework method/constructor) but I'd suggest just living with it.

Why Does it Not Compile?

Because C# first tries to determine the type of your expression Task.Run(() => lst) and then checks whether it is a suitable return type for your method.

  1. lst is a List<string>.
  2. Thus, () => lst is a lambda with a return value of List<string>.
  3. Thus, Task.Run<TResult>(Func<TResult>) called with () => lst infers List<string> as its type parameter TResult.
  4. Thus, Task.Run(() => lst) is of type Task<List<string>>.

You then try to use a value of type Task<List<string>> as the return value of a method returning a Task<IEnumerable<string>>. Task<TResult> is not covariant, hence, you get a compile-time error.

(As an aside: Having a covariant ITask<out TResult> interface would fix this, but the .NET development team decided against it.)

Yes, the compiler could check how you are using the expression created in step 4 and try different interfaces of lst to make it fit, but that's a feature that C# (currently) does not have (and which, in my purely personal opinion, is probably not worth implementing and maintaining due to its low cost-benefit ratio).


To solve this, you can either provide a different type in step 2, by explicitly casting the return type:

return Task.Run(() => (IEnumerable<string>)lst);

or by explicitly declaring the type of the lambda expression:

return Task.Run(new Func<IEnumerable<string>>(() => lst));

or you could override the automatic inference in step 3 with an explicit type argument:

return Task.Run<IEnumerable<string>>(() => lst);

Covariance and contravariance on Tasks

Task<T> is not covariant. Variance can only be applied to generic interfaces (and delegates, but that's not relevant here).

e.g. Task<IEnumerable<Dog>> cannot be assigned to Task<IEnumerable<Animal>>. Because of this, your interface cannot be marked as covariant either.

You might want to see this related question.

Why does Task.FromResult require explicit cast?

Why can't it figure out that my method returns an Task of IList<int>?

Because it doesn't. In this call:

Task.FromResult(new List<int>());

... type inference makes that equivalent to:

Task.FromResult<List<int>>(new List<int>());

So your method is trying to return a Task<List<int>> - and that's not compatible with Task<IList<int>>.

To simplify the point about Task<>, let's use string and object instead, and take out type inference and async entirely. The following code does not compile, and indeed shouldn't:

Task<string> stringTask = Task.FromResult<string>("text");
Task<object> objectTask = stringTask; // Doesn't compile

Task<T> is invariant - there's no conversion from Task<T1> to Task<T2> just because there's a conversion from T1 to T2.

You don't need an explicit cast though - you can just use the implicit conversion earlier:

public Task<IList<int>> TestAsync()
{
// It's important that this variable is explicitly typed as IList<int>
IList<int> result = new List<int>();
return Task.FromResult(result);
}

This uses the implicit conversion from List<int> to IList<int> for the result variable, and then it calls Task.FromResult<IList<int>> using type inference.

An alternative is to keep the method as it was before, except you specify the type argument for Task.FromResult:

public Task<IList<int>> TestAsync()
{
return Task.FromResult<IList<int>>(new List<int>());
}

Can/should TaskTResult be wrapped in a C# 5.0 awaitable which is covariant in TResult?

Task<T> can't be covariant in T, because it's a class. Only interfaces and delegates can have generic variance.

As for whether it's worth doing the wrapping... I guess that depends on how much you use the covariance within your project. I suspect you'll find all the wrapping and unwrapping confusing over time, to be honest - if it's not too bad to just take the hit of removing the covariance, I'd do that.

Cannot convert type 'TaskDerived' to 'TaskInterface'

C# does not allow variance on classes, only interfaces and delegates that are parameterized with reference types. Task<T> is a class.

This is somewhat unfortunate, as Task<T> is one of those rare classes that could be made safely covariant.

However it is easy enough to convert a Task<Derived> to a Task<Base>. Just make a helper method / lambda that takes a Task<Derived> and returns Task<Base>, await the passed-in task, and return the value cast to Base. The C# compiler will take care of the rest. Of course you lose referential identity, but you weren't ever going to get that with a class.

Using Task.FromResult to implicitly convert a TaskT to a TaskX where T : X?

return Task.FromResult<IDictionary<double, double>>(new Dictionary<double, double>());

This is because Task<T> is not covariant, so a Task<Foo> is not interchangeable with a Task<IFoo> even if Foo : IFoo.

You may also wish to consider using ValueTask<T> if you're going to have lots of results which are available synchronously - it is significantly cheaper in that scenario. You can even use the implicit new() usage in that scenario:

ValueTask<IDictionary<double, double>> GetTaskDict()
{
return new(new Dictionary<double, double>());
}

(here, new(...) is interpreted as new ValueTask<IDictionary<double, double>>(...) from the declared return-type of the method)



Related Topics



Leave a reply



Submit