LINQ Выберите аналог для асинхронного метода

2

Допустим, у меня есть следующий пример кода:

private static async Task Main(string[] args)
{
    var result = Enumerable.Range(0, 3).Select(x => TestMethod(x)).ToArray();
    Console.ReadKey();
}

private static int TestMethod(int param)
{
    Console.WriteLine($"{param} before");
    Thread.Sleep(50);
    Console.WriteLine($"{param} after");
    return param;
}

TestMethod будет выполняться до завершения 3 раза, поэтому я увижу 3 пары before и after:

0 before
0 after
1 before
1 after
2 before
2 after

Теперь мне нужно сделать TestMethod асинхронным:

private static async Task<int> TestMethod(int param)
{
    Console.WriteLine($"{param} before");
    await Task.Delay(50);
    Console.WriteLine($"{param} after");
    return param;
}

Как я могу написать подобное выражение Select для этого асинхронного метода? Если я просто использую асинхронную лямбду Enumerable.Range(0, 3).Select(async x => await TestMethod(x)).ToArray(); , это не сработает, потому что не будет ждать завершения, поэтому before будут вызваны части:

0 before
1 before
2 before
2 after
0 after
1 after

Обратите внимание, что я не хочу выполнять все 3 вызова параллельно - мне нужно, чтобы они выполнялись один за другим, начиная следующий, только когда предыдущий полностью закончил и вернул значение.

  • 0
    Как насчет использования foreach ?
  • 0
    @JSteward, безусловно, вариант, но стиль LINQ просто намного короче и элегантнее. Я сделаю метод расширения, оборачивающий foreach , я просто надеялся, что уже есть решение в стандартной библиотеке.
Показать ещё 6 комментариев
Теги:
async-await

5 ответов

-1
Лучший ответ

Похоже, что в настоящее время нет другого решения (до С# 8.0), кроме перечисления его вручную. Я сделал метод расширения для этого (возвращает список вместо массива, как это проще):

public static async Task<List<T>> ToListAsync<T>(this IEnumerable<Task<T>> source)
{
    var result = new List<T>();
    foreach (var item in source)
    {
        result.Add(await item);
    }
    return result;
}
3

В С# 8 (следующая основная версия на момент написания) будет поддерживаться IAsyncEnumrable<T> где вы можете написать async для каждого цикла:

await foreach (var item in GetItemsAsync())
    // Do something to each item in sequence

Я ожидаю, что будет метод расширения Select для проецирования, но если нет, то написать свой собственный не сложно. Вы также сможете создавать блоки итератора IAsyncEnumrable<T> с yield return.

2

Я регулярно сталкиваюсь с этим требованием и не знаю ни одного встроенного решения для его решения, как в С# 7.2. Обычно я просто использую await для каждой асинхронной операции внутри foreach, но вы можете использовать метод расширения:

public static class EnumerableExtensions
{
    public static async Task<IEnumerable<TResult>> SelectAsync<TSource, TResult>(
        this IEnumerable<TSource> source,
        Func<TSource, Task<TResult>> asyncSelector)
    {
        var results = new List<TResult>();
        foreach (var item in source)
            results.Add(await asyncSelector(item));
        return results;
    }
}

Затем вы должны вызвать await на SelectAsync:

static async Task Main(string[] args)
{
    var result = (await Enumerable.Range(0, 3).SelectAsync(x => TestMethod(x))).ToArray();
    Console.ReadKey();
}

Недостатком этого подхода является то, что SelectAsync стремится, а не ленив. С# 8 обещает ввести асинхронные потоки, которые позволят этому снова быть ленивым.

0

Вы должны знать, что такое объект Enumerable. Перечислимый объект, не само перечисление. Это дает вам возможность получить объект Enumerator. Если у вас есть объект Enumerator, вы можете запросить первые элементы последовательности, а как только вы получите перечислимый элемент, вы можете запросить следующий.

Таким образом, создание объекта, который позволяет вам перечислять последовательности, не имеет ничего общего с самим перечислением.

Ваша функция Select возвращает только объект Enumerable, но не запускает перечисление. Перечисление запускается ToArray.

На самом низком уровне перечисление выполняется следующим образом:

IEnumerable<TSource> myEnumerable = ...
IEnumerator<TSource> enumerator = myEnumerable.GetEnumerator();
while (enumerator.MoveNext())
{
     // there is still an element in the enumeration
     TSource currentElement = enumerator.Current;
     Process(currentElement);
}

ToArray будет внутренне вызывать GetEnumerator и MoveNext. Поэтому, чтобы сделать оператор LINQ асинхронным, вам понадобится ToArrayAsync.

Код довольно прост. Я покажу вам ToListAsync как метод расширения, который немного проще сделать.

static class EnumerableAsyncExtensions
{
    public static async Task<List<TSource>> ToListAsync<TSource>(
       this IEnumerable<Task<TSource>> source)
    {
        List<TSource> result = new List<TSource>();
        var enumerator = source.GetEnumerator()
        while (enumerator.MoveNext())
        {
            // in baby steps. Feel free to do this in one step
            Task<TSource> current = enumerator.Current;
            TSource awaitedCurrent = await current;
            result.Add(awaitedCurrent);
        }
        return result;
    } 
}

Вам нужно будет создать это только один раз. Вы можете использовать его для любого ToListAsync, где вам придется ждать каждого элемента:

 var result = Enumerable.Range(0, 3)
     .Select(i => TestMethod(i))
     .ToListAsync();

Обратите внимание, что возвращаемое значение Select - это IEnumerable<Task<int>>: объект, который позволяет перечислять последовательность объектов Task<int>. В foreach каждый цикл вы получаете Task<int>

  • 0
    Интересно, что я сделал не так, чтобы заслужить понижение голоса. Если вы не прокомментируете, когда вы отрицаете, я не могу улучшить свой ответ и никогда не узнаю о том, что не так
  • 0
    Мне тоже интересно, почему кто-то проголосовал. Очень интересная информация о Enumerable , я ее не осознавал. Я пришел к очень похожему ToListAsync расширения ToListAsync , за исключением того, что я использовал foreach вместо прямой работы с Enumerable (см. Ответ, который я добавил). Я понимаю, что foreach основном делает то же самое под капотом. Это правильно, или я что-то пропустил?
Показать ещё 1 комментарий
-3

Вы можете использовать примитивы синхронизации

    class Program
    {
        static async Task Main(string[] args)
        {
            var waitHandle = new AutoResetEvent(true);
            var result = Enumerable
                .Range(0, 3)
                .Select(async (int param) => {
                    waitHandle.WaitOne();
                    await TestMethod(param);
                    waitHandle.Set();
                }).ToArray();
            Console.ReadKey();
        }
        private static async Task<int> TestMethod(int param)
        {
            Console.WriteLine($"{param} before");
            await Task.Delay(50);
            Console.WriteLine($"{param} after");
            return param;
        }
    }

Ещё вопросы

Сообщество Overcoder
Наверх
Меню