5

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);
    }
}
Cerbrus
  • 70,800
  • 18
  • 132
  • 147
  • 2
    This is either covariance or contravariance – Daniel A. White Aug 16 '22 at 11:44
  • 3
    `Task` isn't covariant – DavidG Aug 16 '22 at 11:44
  • 2
    May also be useful https://stackoverflow.com/questions/30996986/why-is-taskt-not-co-variant – DavidG Aug 16 '22 at 11:46
  • Does this answer your question? [Casting List<> of Derived class to List<> of base class](https://stackoverflow.com/questions/3720751/casting-list-of-derived-class-to-list-of-base-class) Essentially the same problem: lack of variance – Charlieface Aug 16 '22 at 14:09
  • @Charlieface no, if anything, DavidG's link is a more suitable duplicate. The whole _"Add a Dog to a list of what used to be cats"_ example isn't relevant here, as we're not talking lists with possible assignment. – Cerbrus Aug 16 '22 at 14:25
  • A class cannot be co- or contra-variant according to the CLR and C# rules, so yes it's the same thing. Theoretically it could have been implemented using interfaces, at significant extra complexity. See also this link https://stackoverflow.com/a/13107168/14868997. You have to remember that if you could in theory create a clss `Task` then that class could not have a `T field;` because it would be writable. Even `readonly` won't help. So that removes the whole point of such a class: to be able to store the result of the task. – Charlieface Aug 16 '22 at 14:33
  • @Charlieface All I'm saying is that the dupe target you proposed is very focused on `List` functionality, which isn't relevant to this question at all. David's link is much more to the point. – Cerbrus Aug 16 '22 at 14:54
  • @DavidG Perhaps vote to close as dupe using your link? – Charlieface Aug 16 '22 at 14:55

1 Answers1

13

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.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • So, it's a deliberate restriction / design choice on `Task`. At least that means I'm not entirely crazy :D Thanks for the quick answer! – Cerbrus Aug 16 '22 at 11:50
  • 1
    @Cerbrus No, all classes in C# are invariant. The compiler _implicitly_ returns a `Task` when you use `async`, but it's `Result` can reference the returned `ChildClass`. – Johnathan Barclay Aug 16 '22 at 11:52
  • 1
    @Cerbrus Well, all classes are invariant so not really deliberate with respect to `Task` – DavidG Aug 16 '22 at 11:52
  • Okay, so if I understand correctly it's the inheritance and conversion from `Task` to `Task` that would require a manual implementation? _"Just living with it"_ then seems like a more efficient choice. – Cerbrus Aug 16 '22 at 11:57
  • 1
    Don't forget arrays can be covariant too e.g. `BaseClass[] items = new ChildClass[5];` – DavidG Aug 16 '22 at 11:57
  • 1
    @Cerbrus [See this](https://stackoverflow.com/questions/15530099/how-to-convert-a-tasktderived-to-a-tasktbase). – Johnathan Barclay Aug 16 '22 at 11:59
  • @DavidG: Have added a note. Array variance is weird and annoying IMO :) – Jon Skeet Aug 16 '22 at 12:03