1

How can I use a Task<string> instance as a Task<string?> parameter to a method?

If I am using nullability enabled, and I have two async methods like..

// in an ordinary class
public async static Task<string> Foo() { ... }

// in a static class
public async static Task Bar(this Task<string?> task1) { ... }

I try to call await Foo().Bar();, but the compiler gives me:

warning CS8620: Argument of type 'Task<string>' cannot be used for parameter 'task' of type 'Task<string?>'

What can I do to the result of Foo to make it acceptable as a Task<string?>?

Patrick Szalapski
  • 8,738
  • 11
  • 67
  • 129
  • I think you are looking for the [! (null-forgiving) operator](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-forgiving) – kapsiR Sep 02 '21 at 20:48
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/236709/discussion-between-kapsir-and-servy). – kapsiR Sep 02 '21 at 21:00
  • @patrick-szalapski You can use it like this: `await FooClass.Foo()!.Bar();` – kapsiR Sep 02 '21 at 21:10
  • Thanks, but I don't understand it. Foo() doesn't return a nullable task (it returns a `Task`, not a `Task?`); why would I need to forgive null on something that isn't nullable? – Patrick Szalapski Sep 03 '21 at 14:30

3 Answers3

2

There is a proposal for Task nullability covariance.

For now, you have to use the ! (null-forgiving) operator to get rid of the compiler warning:
await FooClass.Foo()!.Bar();

class Program
{
    static async Task Main(string[] args)
    {
        await FooClass.Foo()!.Bar();
    }
}

public static class Extensions
{
    public async static Task Bar(this Task<string?> task)
    {
        System.Console.WriteLine("bar");
        await Task.CompletedTask;
    }
}


public class FooClass
{
    public async static Task<string> Foo()
    {
        System.Console.WriteLine("foo");
        return await Task.FromResult("");
    }
}

Full example on SharpLab

kapsiR
  • 2,720
  • 28
  • 36
  • That works, but I don't understand it. Foo() doesn't return a nullable task (it returns a `Task`, not a `Task?`); why would I need to forgive null on something that isn't nullable? – Patrick Szalapski Sep 03 '21 at 14:27
  • I'm not sure myself - I think this can be related to an open issue: [nullable tracking doesn't work well with linq (#37468)](https://github.com/dotnet/roslyn/issues/37468) – kapsiR Sep 04 '21 at 19:36
  • @PatrickSzalapski I left a comment, let's see: https://github.com/dotnet/roslyn/issues/37468#issuecomment-913031293 – kapsiR Sep 04 '21 at 19:54
  • 1
    I provide some explanation as to why this does not work in my answer https://stackoverflow.com/a/69069014/24874 – Drew Noakes Sep 06 '21 at 04:06
  • Yes, there is a proposal for this issue: https://github.com/dotnet/csharplang/issues/3950 – kapsiR Sep 06 '21 at 08:23
2

The issue is that C#'s nullable reference type annotations are not variant for generic types.

That is evidenced by this code:

Task<object> o = Task.FromResult<object>(new object());
Task<object?> p = o;

While we know that a task which cannot return a null should be permissible in place of one which can return a null, the compiler does not know that.

Compiling this gives warning:

CS8619: Nullability of reference types in value of type 'Task' doesn't match target type 'Task<object?>'.

Note that this variance does apply for delegates and interfaces. But Task<T> is neither of those.

For example, both of these compile without warning:

IEnumerable<object> c = new[] { new object() };
IEnumerable<object?> d = c;

Action<object?> a = _ => {};
Action<object> b = a;

There has been discussion about adding special handling to the compiler for nullability of T in Task<T>, but it has not yet landed.

Until that time, you must use the null forgiveness operator to suppress these warnings.

Drew Noakes
  • 300,895
  • 165
  • 679
  • 742
1

You can wrap the Task<string> in a continuation that returns the result as a Task<string?>:

await Foo().ContinueWith<string?>(task => {
    try {
        return task.Result;
    } catch (AggregateException e) {
        throw e.InnerExceptions[0]; // Propagate exceptions/cancellation.
    }
}).Bar();

The lambda expression in ContinueWith must return a string?, which works here because task.Result is a string, which is implicitly convertible to string?.

Spencer Bench
  • 647
  • 4
  • 7
  • 1
    This doesn't maintain proper error/cancellation status of the task. – Servy Sep 02 '21 at 20:44
  • @Servy That's outside the scope of this question. – Spencer Bench Sep 02 '21 at 20:45
  • 2
    How is that outside of the scope of the question? The question doesn't say that the task is never canceled or faulted, or that they don't care about the behavior of the task when it faults or is canceled. – Servy Sep 02 '21 at 20:46
  • @Servy This answer is meant as an illustration, not as a comprehensive solution. I'll add a caveat. – Spencer Bench Sep 02 '21 at 20:50
  • If your solution is incomplete and unfinished, you should absolutely *make that clear* because less experienced users won't realize that you posted a solution that isn't working and won't realize that they need to fix it (let alone how). Additionally, if someone points out that your unfinished answer is unfinished, it's generally preferable to *finish it*, than it is to just say you don't care. – Servy Sep 02 '21 at 20:54
  • The solution works, it just doesn't meet your arbitrary standard of robustness, which you're assuming is appropriate for this scenario. (It may not be.) If I were to hold every SO example to the same standard as my professional work, then they'd be 10x more complex than necessary to teach the core concepts. – Spencer Bench Sep 02 '21 at 21:06
  • The solution *doesn't* work though. It works, *sometimes*, and it doesn't work other times. A solution often not working is sufficient to say it "doesn't work". You're assuming this task never throws and is never cancelled. That's not a warranted assumption. If there were grounds to make that assumption, that would be different. This also *isn't* teaching the proper underlying concepts. It's just suggesting an improper solution and building bad habits, habits they'll just need to break if they want to write code that *actually works*. – Servy Sep 02 '21 at 21:18
  • @Servy "That's not a warranted assumption." Neither is the assumption that it _does_ throw or is cancelled. The question simply doesn't give us that information. Also, _all code we write_ is code that only works sometimes. We _always_ make some degree of assumption about APIs obeying their contracts, memory availability, the runtime behaving correctly, the server not catching on fire, cosmic rays not flipping bits in just the right way to subvert our hardening... Clearly we have different opinions about which degree of assumption is appropriate for SO answers. – Spencer Bench Sep 02 '21 at 22:15
  • I'm not making *any* assumptions about the task. That it might throw is *documented behavior of how tasks work*. That's not me making any special assumptions. Likewise assuming an API obeys their contract isn't making an assumption, it's *them* making the assumption (although those are *warranted* assumptions regardless). Assumptions about cosmic rays snot flipping bits are (in most every context) *warranted* assumptions. You assuming that this task can never throw or be cancelled isn't a *warranted* assumption. – Servy Sep 02 '21 at 22:38
  • A task being canceled or faulting isn't it violating it's contract, **the contract of tasks is that the can be canceled or fault*. When dealing with an arbitrary task of unknown implementation, you have to assume it can fault or be cancelled*, you need a reason to know that a given task *can't* fault or be cancelled in order to ignore those code paths in good faith. – Servy Sep 02 '21 at 22:38
  • 1
    I appreciate the effort and intent, Spencer, but I must agree with Servy--the solution here should be robust enough to permit catching exceptions to be useful in nearly any context. I have edited the question to make that clear. But your answer was helpful and instructive anyway. – Patrick Szalapski Sep 03 '21 at 14:34
  • @PatrickSzalapski I've updated my answer. Turns out it's pretty easy to propagate the exception/cancellation status of the inner task in a way that's sufficient for the vast majority of scenarios. – Spencer Bench Sep 03 '21 at 16:26
  • I would not recommend the approach offered here. It adds run-time overhead for what is essentially a compile-time checking issue, and which therefore should have a zero-run-time-cost solution. The _real_ fix is for the compiler to understand this scenario and not give invalid warnings. Until that time we should manually suppress these issues where they occur. – Drew Noakes Sep 06 '21 at 04:08