Подтвердить что ты не робот

Могу ли я ждать перечислимого мною с генератором?

Скажем, у меня есть последовательность целых чисел, которые я получаю асинхронно.

async Task<int> GetI(int i){
    return await Task.Delay(1000).ContinueWith(x => i);
}

Я хочу создать генератор над этой последовательностью, если бы последовательность была синхронной, я бы сделал:

IEnumerable<int> Method()
{
    for (var i = 0; i < 100; i++)
    {
        yield return GetI(i); // won't work, since getI returns a task
    }
}

Итак, я решил, что аналогия делает генератор асинхронным и уступает ему:

async Task<IEnumerable<int>> Method()    
{
    for (var i = 0; i < 100; i++)
    {
        yield return await Task.Delay(1000).ContinueWith(x => i);
    }
}

Это не сработает, так как метод с yield должен возвращать IEnumerable что-то, альтернатива, которая имеет больше смысла IEnumerable<Task<int>>, но не будет компилироваться, поскольку методы async должны возвращать Task или void.

Теперь я понимаю, что могу просто удалить ожидание и вернуть IEnumerable<Task<int>>, но это не поможет мне, так как итерация будет продолжать запрашивать данные, прежде чем какая-либо из них будет готова, поэтому она не решит мою проблему.

  • Есть ли способ хорошо смешивать перечисления и задачи с приятным сахаром, который дает мне язык с ожиданием и выходом?
  • Есть ли способ хорошо его использовать?

(Из поиска в Интернете я подозреваю, что ответ на первый вопрос ложный, а второй - наблюдатель/наблюдаемый, но я не нашел никакой канонической ссылки, и мне интересен лучший способ реализовать этот шаблон в С#)

4b9b3361

Ответ 1

Интересны асинхронные последовательности. Существует несколько разных подходов, в зависимости от того, что вы хотите сделать. Я не совсем понимаю вашу желаемую семантику, поэтому это некоторые из параметров.

Task<IEnumerable<T>> - это асинхронно полученная коллекция. Существует только одна задача - одна асинхронная операция, которая извлекает всю коллекцию. Это не похоже на то, что вы хотите.

IEnumerable<Task<T>> - (синхронная) последовательность (асинхронных) данных. Существует несколько задач, которые могут или не все могут обрабатываться одновременно. Есть несколько вариантов реализации этого. Один использует блок перечислителя и дает задания; этот подход начнет новую асинхронную операцию каждый раз, когда следующий элемент будет извлечен из перечислимого. Кроме того, вы можете создавать и возвращать коллекцию задач со всеми выполняемыми одновременно задачами (это можно сделать элегантно над исходной последовательностью через LINQ Select, за которой следует ToList/ToArray). Тем не менее, это имеет несколько недостатков: нет способа асинхронно определять, завершена ли последовательность, и нелегко сразу начать следующую обработку элемента после возврата текущего элемента (что обычно является желательным поведением).

Основная проблема заключается в том, что IEnumerable<T> по своей сути является синхронным. Есть несколько обходных решений. Один из них - IAsyncEnumerable<T>, который является асинхронным эквивалентом IEnumerable<T> и доступен в пакете Ix-Async NuGet. Однако этот подход имеет свои недостатки. Конечно, вы теряете хорошую поддержку языка для IEnumerable<T> (а именно, блоков перечислителя и foreach). Кроме того, само понятие "асинхронного перечислимого" не совсем соответствует действительности; в идеале, асинхронные API должны быть короткими, а не чатными, а перечисления - очень частыми. Больше обсуждений оригинального дизайна здесь, а на подробные/частые соображения здесь.

Итак, в наши дни гораздо более распространенным решением является использование наблюдаемых или потоки данных (оба доступны также через NuGet). В этих случаях вы должны думать о "последовательности" как о чем-то с собственной жизнью. Наблюдаемые данные основаны на нажатиях, поэтому код потребления (идеально) реагирует. В потоках данных есть ощущение актера, поэтому они действуют более независимыми, снова нажимая результаты на потребительский код.

Ответ 2

Вы можете вернуть IEnumerable<Task<int>>:

IEnumerable<Task<int>> Method()
{
    for (var i = 0; i < 100; i++)
    {
        yield return Task.Delay(1000).ContinueWith(x => i);
    }
}

И назовите его следующим образом:

foreach(var i in Method())
    Console.WriteLine(await i);