Как увлажнить словарь с результатами асинхронных вызовов?

Предположим, что у меня есть код, который выглядит так:

public async Task<string> DoSomethingReturnString(int n) { ... }
int[] numbers = new int[] { 1, 2 , 3};

Предположим, что я хочу создать словарь, содержащий результат вызова DoSomethingReturnString для каждого числа, подобного этому:

Dictionary<int, string> dictionary = numbers.ToDictionary(n => n,
    n => DoSomethingReturnString(n));

Это не сработает, потому что DoSomethingReturnString возвращает Task<string>, а не string. Intellisense предположил, что я пытаюсь указать, что мое лямбда-выражение является асинхронным, но это также не помогло решить проблему.

Ответ 1

Если вы настаиваете на том, чтобы делать это с помощью linq, Task.WhenAll является ключом для "гидратации" словаря:

int[] numbers = new int[] { 1, 2 , 3};

KeyValuePair<int, string>[] keyValArray = //using KeyValuePair<,> to avoid GC pressure
    await Task.WhenAll(numbers.Select(async p => 
        new KeyValuePair<int, string>(p, await DoSomethingReturnString(p))));

Dictionary<int, string> dict = keyValArray.ToDictionary(p => p.Key, p => p.Value);

Ответ 2

Методы LINQ не поддерживают асинхронные действия (например, асинхронные селектора значений), но вы можете создать их самостоятельно. Вот многоразовый метод расширения ToDictionaryAsync, который поддерживает селектор асинхронного значения:

public static class ExtensionMethods
{
    public static async Task<Dictionary<TKey, TValue>> ToDictionaryAsync<TInput, TKey, TValue>(
        this IEnumerable<TInput> enumerable,
        Func<TInput, TKey> syncKeySelector,
        Func<TInput, Task<TValue>> asyncValueSelector)
    {
        Dictionary<TKey,TValue> dictionary = new Dictionary<TKey, TValue>();

        foreach (var item in enumerable)
        {
            var key = syncKeySelector(item);

            var value = await asyncValueSelector(item);

            dictionary.Add(key,value);
        }

        return dictionary;
    }
}

Вы можете использовать его следующим образом:

private static async Task<Dictionary<int,string>>  DoIt()
{
    int[] numbers = new int[] { 1, 2, 3 };

    return await numbers.ToDictionaryAsync(
        x => x,
        x => DoSomethingReturnString(x));
}

Ответ 3

Если вы вызываете асинхронный метод, вы можете написать метод-оболочку, который создает новый словарь и создает словарь, итерируя по каждому номеру, вызывая в свою очередь DoSomethingReturnString:

public async Task CallerAsync()
{
    int[] numbers = new int[] { 1, 2, 3 };
    Dictionary<int, string> dictionary = await ConvertToDictionaryAsync(numbers);
}

public async Task<Dictionary<int, string>> ConvertToDictionaryAsync(int[] numbers)
{
    var dict = new Dictionary<int, string>();

    for (int i = 0; i < numbers.Length; i++)
    {
        var n = numbers[i];
        dict[n] = await DoSomethingReturnString(n);
    }

    return dict;
}

Ответ 4

Это всего лишь сочетание ответов @Yacoub и @David для метода расширения, который использует Task.WhenAll

public static async Task<Dictionary<TKey, TValue>> ToDictionaryAsync<TInput, TKey, TValue>(
    this IEnumerable<TInput> enumerable,
    Func<TInput, TKey> syncKeySelector,
    Func<TInput, Task<TValue>> asyncValueSelector)
{
    KeyValuePair<TKey, TValue>[] keyValuePairs = await Task.WhenAll(
        enumerable.Select(async input => new KeyValuePair<TKey, TValue>(syncKeySelector(input), await asyncValueSelector(input)))
    );
    return keyValuePairs.ToDictionary(pair => pair.Key, pair => pair.Value);
}