Невозможно дважды выполнить перечисление IAsyncEnumerable?

Невозможно выполнить перечисление IAsyncEnumerable дважды?

После запуска CountAsync await foreach не будет перечислять какие-либо элементы. Почему? Вроде на AsyncEnumerator нет метода сброса.

var count = await itemsToImport.CountAsync();

await foreach (var importEntity in itemsToImport)
{
    // won't run
}

Источник данных:

private IAsyncEnumerable<TEntity> InternalImportFromStream(TextReader reader)
{
    var csvReader = new CsvReader(reader, Config);
        
    return csvReader.GetRecordsAsync<TEntity>();
}

person Sven Krauter    schedule 17.03.2020    source источник
comment
Даже перечислители синхронизации почти никогда не реализуют .Reset(); IAsyncEnumerator просто систематизирует практику, которую вы не можете перечислить более одного раза. Что касается IAsyncEnumerable, то же самое, что и для IEnumerable: возможно ли перечисление более одного раза, не определено в интерфейсе, но для многих источников вы не можете этого сделать, потому что это может привести к скрытому снижению производительности или непоследовательным результатам (например, выполнение базы данных запрос дважды). Вы должны иметь дело с этим явно, что означает, что вы должны либо материализовать результат (.ToList() и т.п.), либо повторить операцию самостоятельно.   -  person Jeroen Mostert    schedule 17.03.2020
comment
Если вам не нужен счетчик для начала, вы можете перебирать каждый элемент, увеличивая счетчик на единицу, поэтому вам не нужно переносить все в память, при этом получая счет после обработки коллекции.   -  person Kieran Devlin    schedule 17.03.2020
comment
Мне нужно посчитать перед перечислением. IEnumerator.Reset существует, IAsyncEnumerator не имеет ничего похожего. Итак, как мне получить счет из этого, потому что я хочу отображать прогресс в форме, такой как запись процесса x вместо счетчика, вместо обработки записи x   -  person Sven Krauter    schedule 17.03.2020
comment
Это превращается в проблему XY. Нам нужно больше подробностей о том, откуда исходит перечисление, чтобы мы могли сделать вывод о том, как вы могли бы достичь своих целей. то есть откуда исходит источник данных и какой клиент вы используете для доступа к указанному источнику данных? (Обновите вопрос)   -  person Kieran Devlin    schedule 17.03.2020
comment
это обычная проблема с IAsyncEnumerable. это не сложный случай   -  person Sven Krauter    schedule 17.03.2020
comment
Для некоторых источников вы просто не можете получить счетчик до перечисления (например, потоковая передача строк из БД). .Reset(), даже если он существует, требует, чтобы результаты где-то буферизовались или все было переделано. Если вы хотите буферизовать / повторить их самостоятельно, вы можете, но вы не можете ожидать, что источник сделает это за вас, если вы захотите дождаться подсчета. Коллекции реализуют свои собственные .Count свойства, которые вы можете использовать для непосредственного получения счетчика; аналогичный механизм можно использовать для других источников, в зависимости от их характера (например, отдельный Count вызов веб-API).   -  person Jeroen Mostert    schedule 17.03.2020
comment
было бы достаточно, если бы я мог перечислить его дважды, но без обработки и просто подсчета. этот интерфейс поступает из загруженных данных библиотеки CsvHelper.   -  person Sven Krauter    schedule 17.03.2020
comment
Если данные фактически уже загружены (например, находятся в памяти), их асинхронное перечисление в любом случае ничего не добавляет, и вы также можете использовать перечисление синхронизации (которое, предположительно, можно повторить). С другой стороны, если он не является резидентным и используется перечисление async для инкапсуляции асинхронного (файлового) ввода-вывода, что было бы гораздо более распространено, вы должны сделать сознательный выбор, чтобы прочитать файл дважды, и интерфейс вынуждает делать это явно.   -  person Jeroen Mostert    schedule 17.03.2020
comment
Спасибо за это. Я помещу его один раз в список, а затем перечислю его синхронно после того, как использую значение счетчика.   -  person Sven Krauter    schedule 17.03.2020
comment
Как сказал Дэвид Браун, это не из-за IAsyncEnumerable, а из-за базового типа, который его реализует. Я рекомендую прочитать эту статью. Есть пример, который работает независимо от того, сколько раз он повторяется: docs.microsoft.com/en-us/archive/msdn-magazine/2019/november/   -  person Fabjan    schedule 17.03.2020
comment
Добавляя ко всем комментариям, IAsyncEnumerable представляет активный поток результатов. Это может продолжаться вечно. Чтобы перечислить это дважды, издатель данных должен будет выполнить дважды - вам придется снова вызвать ту же последовательность HTTP API, или выполнить тот же запрос еще раз, или снова прочитать тот же поток ввода-вывода. .   -  person Panagiotis Kanavos    schedule 18.03.2020


Ответы (2)


Это не имеет ничего общего с сбросом IAsyncEnumerator. Этот код пытается сгенерировать второй IAsyncEnumerator, который, как и IEnumerable.GetEnumerator (), возможен только для некоторых видов коллекций. Если Enumerable (асинхронный или нет) является абстракцией над какой-то структурой данных, предназначенной только для пересылки, то GetEnumerator / GetAsyncEnumerator завершится ошибкой.

И даже если это не выходит из строя, иногда это дорого. Например, он может запускать запрос к базе данных или обращаться к удаленному API каждый раз при его перечислении. Вот почему IEnumerable / IAsyncEnumerable делает плохие общедоступные возвращаемые типы из функций, поскольку они не могут описать возможности возвращаемой коллекции, и почти единственное, что вы можете сделать со значением, - это материализовать его с помощью .ToList / ToListAsync.

Например, это отлично работает:

static async IAsyncEnumerable<int> Col()
{
    for (int i = 1; i <= 10; i++)
    {
        yield return i;
    }
}
static void Main(string[] args)
{
    Run().Wait();
}
static async Task Run()
{

    var col = Col();

    var count = await col.CountAsync();
    await foreach (var dataPoint in col)
    {
        Console.WriteLine(dataPoint);
    }
}
person David Browne - Microsoft    schedule 17.03.2020

Кажется, что невозможно сбросить IAsyncEnumerable самим интерфейсом из-за того, что в интерфейсе IAsyncEnumerator нет метода Reset.

В этом конкретном примере второе перечисление не будет работать, потому что IAsyncEnumerable нацелен на Stream. После того, как поток был прочитан, курсор позиции нацеливается на конец потока. если у вас есть контроль над потоком или ссылка на него (чего у меня нет), вы можете снова установить позицию на 0 и пересчитать ее снова.

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

person Sven Krauter    schedule 17.03.2020
comment
IAsyncEnumerator предназначен для одноразового использования. Это не означает, что IAsyncEnumerable можно перечислить только один раз, потому что перечисляемое - это фабрика, которая может создавать бесконечное количество перечислителей. Вы получаете новый перечислитель каждый раз, когда вызываете _ 3_ метод. Однако нет гарантии, что все эти счетчики будут производить одинаковую последовательность. - person Theodor Zoulias; 18.03.2020
comment
Как я уже сказал: для потока это не сработает, так как позиция потока находится в конце и должна быть снова установлена ​​в 0, прежде чем пытаться повторить его снова. - person Sven Krauter; 28.05.2021
comment
Свен это зависит от реализации. Можно легко реализовать IAsyncEnumerable<T>, который каждый раз при перечислении создает новый CsvReader и начинает перебирать его с позиции 0. Для этого вам просто нужно добавить модификатор async к InternalImportFromStream, и цикл и yield элементы, содержащиеся в GetRecordsAsync метод. - person Theodor Zoulias; 28.05.2021
comment
... вот так: Сквозная передача для IAsyncEnumerable? - person Theodor Zoulias; 28.05.2021