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

Time:08-16

I'm trying to return a specifically typed value from a generic function (GenericGetter).

When I try to simply return the typed Task result from GenericGetter, the compiler is showing me the following error:

Cannot convert expression type 'System.Threading.Tasks.Task<Example.ChildClass>' 
    to return type 'System.Threading.Tasks.Task<Example.BaseClass>'

However, if I make the function that contains the GenericGetter call async, and I return the awaited value instead, the compiler doesn't mind. It compiles, it works, but to me it seems the added async / await are redundant.

Why does GetChildClass not compile, while GetChildClassAsync does?

Here's my example:

namespace Example
{
    public class BaseClass {}
    public class ChildClass : BaseClass {}

    public class MyExample
    {
        private async Task Main()
        {
            var foo = await GetChildClass().ConfigureAwait(false);
            var bar = await GetChildClassAsync().ConfigureAwait(false);
        }

        private Task<BaseClass> GetChildClass() =>
            GenericGetter<ChildClass>();

        private async Task<BaseClass> GetChildClassAsync() =>
            await GenericGetter<ChildClass>().ConfigureAwait(false);

        private Task<T> GenericGetter<T>()
            where T : BaseClass =>
            Task.FromResult<T>(null);
    }
}

CodePudding user response:

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.

  • Related