0

There's a question from a few years back How to WhenAll when some tasks can be null? which has a fairly simple top-rated answer:

Task task1 = null
await Task.WhenAll(new Task[] { task1, task2, task3 }.Where(i => i != null));

But in modern .Net 6 we have NRTs and this causes compiler warnings because you are putting Task? inside an array of Task.

If you simply update this to:

await Task.WhenAll(new Task?[] { task1, task2, task3 }.Where(i => i != null));

You still get a warning because Task.WhenAll doesn't accept Task?. What is the modern NRT version of the referenced solution?

MRE here: https://dotnetfiddle.net/R2SN43

using System;
using System.Threading.Tasks;
using System.Linq;

#nullable enable
public class Program
{
    public static async void Main()
    {
        Task? task1 = null;
        Task? task2 = null;
        Task? task3 = null;
        
        await Task.WhenAll(new Task?[] { task1,task2,task3 }.Where(t => t is not null));
    }
}

This is not a question about converting a list of NRTs to non-nullable. It is a question about Tasks that may not be run. Ending up with a list of nullable Tasks is a symptom, not the specific problem... as one commenter has mentioned we might avoid having null values entirely.

Mr. Boy
  • 60,845
  • 93
  • 320
  • 589
  • 1
    Can you elaborate why you have `Task?` instances in the first place? Why would you ever return a `null` task instead of something like `Task.CompletedTask`? – julealgon Jan 03 '23 at 13:43
  • @julealgon see the referenced question, but essentially you might have Tasks which are optional.. `Task t = id >0 ? GetObject(id) : null` – Mr. Boy Jan 03 '23 at 14:06
  • Still, why not return `Task.CompletedTask` instead of `null`? – julealgon Jan 03 '23 at 17:32
  • 1
    @julealgon why not submit this as an answer since it seems to be a nice approach? – Mr. Boy Jan 04 '23 at 10:36
  • Sounds good. I'll post as an answer once the question is reopened (I agree it was incorrectly closed so also voted to reopen it). – julealgon Jan 04 '23 at 13:04
  • 1
    @julealgon I have reopened the question. – Guru Stron Jan 04 '23 at 18:05
  • @Mr.Boy you need to be more specific/precise when writing questions so they are not closed as duplicates, because the original one was the clear duplicate. – Guru Stron Jan 04 '23 at 18:07

2 Answers2

1

Have you tried adding a Cast<Task>() at the end, like this?

    await Task.WhenAll(new Task?[] { task1,task2,task3 }.Where(t => t is not null).Cast<Task>());
Tanveer Badar
  • 5,438
  • 2
  • 27
  • 32
  • I actually wasn't aware of the `.Cast()` LINQ method. Interesting. I had also considered adding `.Select(t => t!)` but it seems a little ugly. – Mr. Boy Jan 03 '23 at 13:40
  • 2
    @Tanveer Badar: Isn't better the use OfType it does the same as your where then cast. – Yevhen Cherkes Jan 03 '23 at 14:26
  • There's a difference between semantics of `Cast()` and `OfType()` which only the OP can choose. `Cast()` will throw exceptions while `OfType()` will filter out other types. You can think of them as C# 2.0's `(T)` vs `as` style casts. – Tanveer Badar Jan 04 '23 at 07:55
  • @Mr.Boy All three approaches are nearly equivalent since all we are trying to do is make the compiler shut up since we know better in this case, whichever way you want to proceed with must depend on the larger context not present in the question. – Tanveer Badar Jan 04 '23 at 07:57
1

I believe the best approach for this would be to avoid the null in the first place.

There are basically 2 ways in which one could avoid them from my perspective:

  1. Use Task.CompletedTask

    Think of Task.CompletedTask as the null-object implementation of a Task: a neutral, zero-cost version of a Task that "does the default thing" (succeed). Whatever returns the "optional" tasks today, could be changed to return Task.CompletedTask instead of null, meaning you just await those as normal (they are immediately returned as successful).

  2. Use an iterator and do not return the optional tasks

    The second option that I see depends on whether or not the list of tasks is "dynamic" or generated by some process and returned as a collection. You could just opt into not returning the task:

    public IEnumerable<Task> GetTasks()
    {
        if (shouldRunTask1)
        {
            yield return Task1();
        }
    
        yield return Task2();
    
        ...
    
        if (shouldRunTask20)
        {
            yield return Task20();
        }
    }
    

If for any reason you still want to handle the null Tasks, you can tell the compiler that the list doesn't contain null's by using the "bang operator"

await Task.WhenAll(new[] { task1, task2, task3 }.Where(t => t is not null)!);

This will get rid of the warning, although I wouldn't recommend it in place of the other solutions.

I prefer this to the Cast option though as the operator was created explicitly for this purpose, while the Cast could be seen as confusing and unnecessary computation-wise.

julealgon
  • 7,072
  • 3
  • 32
  • 77