可以使用新的 C# 语言功能来清理 Task.WhenAll 语法吗?

Can new C# language features be used to to clean-up Task.WhenAll syntax?

有了 "async everywhere",触发多个异构操作的能力变得越来越频繁。当前的 Task.WhenAll 方法 return 将其结果作为一个数组,并要求所有任务都 return 同一类型的对象,这使得它的使用有点笨拙。我 喜欢 能够写...

var (i, s, ...) = await AsyncExtensions.WhenAll(
                          GetAnIntFromARemoteServiceAsync(),
                          GetAStringFromARemoteServiceAsync(),
                          ... arbitrary list of tasks   
                         );
Console.WriteLine($"Generated int {i} and string {s} ... and other things");

我能想出的最好的实现是

public static class AsyncExtensions
{
  public static async Task<(TA , TB )> WhenAll<TA, TB>(Task<TA> operation1, Task<TB> operation2)
  {
             return (await operation1, await operation2;
  }
}

这样做的缺点是我需要实现最多 N 个参数的单独方法。根据 this answer 这只是使用泛型的限制。此实现还有一个限制,即无法支持 void-returning 任务,但这不是一个问题。

我的问题是:是否有任何即将推出的语言功能允许对此采取更简洁的方法?

有一个open feature-request for this on the dotnet/csharplang repository

该问题还提到了另一个开放的功能请求,tuple splatting which could help, to some extent. How, is explained here

这两个问题目前都被标记为 [讨论] 和 [功能请求],并且已经“闲置”了一年(2017 年 5 月 - 2018 年 5 月)。

因此我推断答案(目前)是“否”。


采用扩展方式 Joseph Musser 确实写了很多这些供我们复制和粘贴:https://gist.github.com/jnm2/3660db29457d391a34151f764bfe6ef7

从 .NET 6 开始,标准库中没有 API 允许 await 多个异构任务,并在值元组中获取它们的结果。

我想指出的是,您在问题中展示的实现是不正确的。

// Incorrect
public static async Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2)
{
    return (await task1, await task2);
}

这不是 WhenAll。这是WhenAllIfSuccessful_Or_WhenFirstFails。如果 task1 失败,错误将立即传播,并且 task2 将成为 fire-and-forget 任务。在某些情况下,这可能正是您想要的。但通常您不想忘记您的任务,让它们 运行 在后台不被观察到。在继续下一步工作之前,您希望等待所有这些完成。这是实现 WhenAll 方法的更好方法:

// Good enough
public static async Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2)
{
    await Task.WhenAll(task1, task2).ConfigureAwait(false);
    return (task1.Result, task2.Result);
}

这将等待两个任务完成,如果失败,它将传播第一个失败任务的错误(参数列表中的第一个,不是按时间顺序排列)。在大多数情况下,这完全没问题。但是,如果您发现自己处于需要传播所有异常的情况下,它就会变得棘手。下面是我所知道的最短的实现,它精确地模仿了原生 Task.WhenAll:

的行为
// Best
public static Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2)
{
    return Task.WhenAll(task1, task2).ContinueWith(t =>
    {
        if (t.IsCanceled)
        {
            // Propagate the correct token
            CancellationToken ct = default;
            try { t.GetAwaiter().GetResult(); }
            catch (OperationCanceledException oce) { ct = oce.CancellationToken; }
            return Task.FromCanceled<(T1, T2)>(ct);
        }
        if (t.IsFaulted)
        {
            var tcs = new TaskCompletionSource<(T1, T2)>();
            tcs.SetException(t.Exception.InnerExceptions);
            return tcs.Task;
        }
        return Task.FromResult((task1.Result, task2.Result));
    }, default, TaskContinuationOptions.ExecuteSynchronously |
        TaskContinuationOptions.DenyChildAttach, TaskScheduler.Default).Unwrap();
}