如何将其他上下文信息与任务一起传递给 Task.WhenAll?

How to pass additional context information along with tasks to Task.WhenAll?

考虑以下理想代码(它不起作用)。我实际上是在尝试创建一个 Task 的列表,其中 return 是一个特定对象,将它们与 string 标识符相关联,然后使用 Task.WhenAll 批量执行所有这些.在执行结束时,我需要那些 Task 结果 仍然与它们最初创建的字符串标识符相关联:

public async Task<SomeObject> DoSomethingAsync(string thing)
{
    // implementation elided
}

public async Task<SomeObject> DoSomethingElseAsync(string thing)
{
    // different implementation elided
}

public async Task<IEnumerable<(string, SomeObject)>>
    DoManyThingsAsync(IEnumerable<string> someListOfStrings, bool condition)
{
    var tasks = new List<(string, Task<SomeObject>)>();

    foreach (var item in someListOfStrings)
    {
        if (condition)
        {
            tasks.Add((item, DoSomethingAsync(item)));
        }
        else
        {
            tasks.Add((item, DoSomethingElseAsync(item)));
        }
    }

    // this doesn't compile, i'm just demonstrating what i want to achieve
    var results = await Task.WhenAll(tasks);

    return results;
}

这可以重写为:

public async Task<(string, SomeObject)> DoSomethingWrapperAsync(string thing)
    => (thing, await DoSomethingAsync(thing));

public async Task<(string, SomeObject)> DoSomethingElseWrapperAsync(string thing)
    => (thing, await DoSomethingElseAsync(thing));

public async Task<IEnumerable<(string, SomeObject)>>
    DoManyThingsAsync(IEnumerable<string> someListOfStrings, bool condition)
{
    var tasks = new List<Task<(string, SomeObject)>>();

    foreach (var thing in someListOfStrings)
    {
        if (condition)
        {
            tasks.Add(DoSomethingWrapperAsync(thing));
        }
        else
        {
            tasks.Add(DoSomethingElseWrapperAsync(thing));
        }
    }

    // this does compile
    var results = await Task.WhenAll(tasks);

    return results;
}

问题是我需要为我将要调用的每个可能的离散异步函数提供一个额外的包装方法,这让人感觉没有必要和浪费,而且代码很多(因为会有很多这样的方法)。有没有更简单的方法来实现我的需要?

我研究了 awaitable/awaiter 模式的实现,但看不出如何让它与需要 Task 或 [=19= 的集合的 Task.WhenAll 一起工作], 因为 the guidance seems to be "don't extend those classes".

据我所知,您正在使用 condition 来确定正在调用哪个方法(具有完全相同的签名)。为什么不传递为每个项目调用的回调,而不是在 foreach 循环中执行逻辑?

public async Task<SomeObject> DoSomethingAsync(string thing)
{
    // ...
}

public async Task<SomeObject> DoSomethingElseAsync(string thing)
{
    // ...
}

public async Task<IEnumerable<(string, SomeObject)>> DoManyThingsAsync(IEnumerable<string> someListOfStrings, bool condition)
{
    Func<string, Task<SomeObject>> callback = condition ? DoSomethingAsync : DoSomethingElseAsync;

    var results = await Task.WhenAll(
        someListOfStrings.Select(async thing => (thing, await callback(thing)))
    );
    return results;
}

此外,您还可以将其提取为扩展方法。

public static class AsyncExtensions
{
    public static async Task<IEnumerable<(T, TResult)>> WhenAllAsync(this IEnumerable<T> collection, Func<T, Task<TResult>> selector)
    {
        var results = await Task.WhenAll(
            collection.Select(async item => (item, await selector(item)))
        );
        return results;
    }
}

public async Task MyMethodAsync()
{
    // ...

    var results = await myListOfStrings.WhenAllAsync(condition ? DoSomethingAsync : DoSomethingElseAsync);

    // ...
}

或者在回调中做映射

public static class AsyncExtensions
{
    public static Task<IEnumerable<TResult>> WhenAllAsync(this IEnumerable<T> collection, Func<T, Task<TResult>> selector)
        => Task.WhenAll(collection.Select(selector)));
}

public async Task MyMethodAsync()
{
    // ...

    var results = await myListOfStrings.WhenAllAsync(async thing => (thing, await (condition ? DoSomethingAsync(thing) : DoSomethingElseAsync(thing))));

    // ...
}

您可以边走边压缩:

public async Task<IEnumerable<(string, SomeObject)>>
    DoManyThingsAsync(IEnumerable<string> someListOfStrings, bool condition)
{
  var tasks = someListOfStrings
      .Select(async item =>
          condition ?
          (item, await DoSomethingAsync(item)) :
          (item, await DoSomethingElseAsync(item)))
      .ToList();
  return await Task.WhenAll(tasks);
}

或者,您可以将输入保存为单独的集合,稍后再压缩:

public async Task<IEnumerable<(string, SomeObject)>>
    DoManyThingsAsync(IEnumerable<string> someListOfStrings, bool condition)
{
  // Reify input so we don't enumerate twice.
  var input = someListOfStrings.ToList();

  var tasks = input
      .Select(item =>
          condition ?
          DoSomethingAsync(item) :
          DoSomethingElseAsync(item))
      .ToList();
  var taskResults = await Task.WhenAll(tasks);

  return input.Zip(taskResults, (item, taskResult) => ((item, taskResult)));
}