Dado um IAsyncEnumerable<>
, em geral funciona bem (mesmo que possa ser um desempenho não ótimo) chamar GetAsyncEnumerator
mais de uma vez, obtendo um novo IAsyncEnumerator<>
a cada vez. Para dar um exemplo explícito (todo o código nesta questão está em C#), suponha que eu tenha este método:
static async IAsyncEnumerable<string> GetStr() {
var item = await Task.FromResult("Bravo");
yield return "Alfa";
yield return item;
yield return "Charlie";
}
então funciona bem fazer isso:
IAsyncEnumerable<string> oneEnumerable = GetStr();
await foreach (var s in oneEnumerable) {
Console.WriteLine(s);
}
Console.WriteLine("Between");
await foreach (var t in oneEnumerable) {
Console.WriteLine(t);
}
Console.WriteLine("End");
Se você quiser avançar, você pode até mesmo ter os dois enumeradores (do mesmo enumerável) vivendo juntos, sem descartar o primeiro antes de adquirir o segundo, digamos:
IAsyncEnumerable<string> oneEnumerable = GetStr();
IAsyncEnumerator<string> e = oneEnumerable.GetAsyncEnumerator();
IAsyncEnumerator<string> f = oneEnumerable.GetAsyncEnumerator();
bool c = await f.MoveNextAsync();
bool b = await e.MoveNextAsync();
Console.WriteLine($"b {b} with {e.Current}");
Console.WriteLine($"c {c} with {f.Current}");
await e.DisposeAsync();
await f.DisposeAsync();
O código acima funciona da maneira que vocês esperam, os dois enumeradores são independentes e não misturam seus estados.
Então, isso foi apenas uma introdução; quero perguntar sobre um caso em que adquirir dois enumeradores do mesmo enumerável leva a um resultado estranho. Então, agora considere este método baseado em Task.WhenEach :
static IAsyncEnumerable<Task<int>> GetTasks() {
IEnumerable<Task<int>> source = [Task.FromResult(7), Task.FromResult(9), Task.FromResult(13)];
return Task.WhenEach(source);
}
e use este código:
IAsyncEnumerable<Task<int>> oneEnumerable = GetTasks();
await foreach (var t in oneEnumerable) {
Console.WriteLine(t);
}
Console.WriteLine("Between");
await foreach (var u in oneEnumerable) {
Console.WriteLine(u);
}
Console.WriteLine("End");
Isso é executado sem exceção. MAS: A segunda enumeração fornece zero itens (corpo de foreach com u
execução zero vezes)!
Minhas perguntas:
- Esse é o comportamento esperado de
Task.WhenEach
? - Esse comportamento está documentado?
Acho isso muito propenso a erros. Se, por alguma razão técnica, for impossível obter mais de uma enumeração de um enumerável, seria muito melhor se a segunda chamada para GetAsyncEnumerator
lançasse uma exceção (ou, alternativamente, a primeira chamada para MoveNextAsync
no segundo enumerador lançasse, ou, aguardando o ValueTask<bool>
produzido por MoveNextAsync
lançaria).
(Foi-me mostrado outro tópico onde um IAsyncEnumerable<>
produzido por um GetRecordsAsync<>
de a CsvReader
teve um problema semelhante .)
Parece que ainda não está documentado, mas os comentários do código mencionam isso explicitamente:
Também:
Concordo que isso parece não óbvio e deve ser documentado. De preferência, deve permitir múltiplas enumerações. Sugiro que você crie um feature-request no GitHub para isso.
Você pode ser capaz de escrever seu próprio iterador para fazer isso. Exemplo:
Os métodos iteradores do C# criam enumeráveis que invocam o corpo do método toda vez que uma nova enumeração é iniciada.
A resposta de Charlieface explica o comportamento de
Task.WhenEach
, e inclui uma solução especializada. Esta resposta é sobre uma solução geral para todosIAsyncEnumerable<T>
os s que têm a mesma peculiaridade com oTask.WhenEach
. O pacote System.Interactive.AsyncAsyncEnumerableEx.Defer
inclui o método, com esta assinatura:Com a ajuda deste método, você pode criar uma sequência enumerável adiada que cria uma nova
IAsyncEnumerable<TSource>
cada vez que é enumerada.Exemplo de uso:
Aqui está uma demonstração online. Ela produz esta saída: