Относительно кода моста, необходимого для работы перечислимых Async, я опубликовал NuGet пару дней назад, который делает именно это: CSharp8Beta.AsyncIteratorPrerequisites.Unofficial
Вопреки распространенному мнению,следующий код фактически дает ожидаемые результаты:
private static async IAsyncEnumerable<int> GetNumbersAsync()
{
var nums = Enumerable.Range(0, 10).ToArray();
foreach (var num in nums)
{
await Task.Delay(100);
yield return num;
}
}
, и это потому, что IEnumerable<int>
материализуется в массив int
.То, что на самом деле завершается после двух итераций, заключается в переборе самой IEnumerable<int>
следующим образом:
var nums = Enumerable.Range(0, 10); // no more .ToArray()
foreach (var num in nums) {
Тем не менее, хотя превращение запросов в материализованные коллекции может показаться умным приемом, это не всегда так.Вы хотели бы буферизовать всю последовательность (таким образом, теряя память и время).
Учитывая производительность, я обнаружил, что почти обертка с нулевым распределением над IEnumerable
, котораяпревратит его в IAsyncEnumerable
плюс, используя await foreach
вместо просто foreach
, чтобы обойти эту проблему.
Недавно я опубликовал новую версию пакета NuGet, которая теперь включает метод расширения под названием ToAsync<T>()
для IEnumerable<T>
в целом, помещается в System.Collections.Generic
, что делает именно это.Подпись метода:
namespace System.Collections.Generic {
public static class EnumerableExtensions {
public static IAsyncEnumerable<T> ToAsync<T>(this IEnumerable<T> @this)
, и после добавления пакета NuGet в проект .NET Core 3 его можно использовать следующим образом:
using System.Collections.Generic;
...
private static async IAsyncEnumerable<int> GetNumbersAsync() {
var nums = Enumerable.Range(0, 10);
await foreach (var num in nums.ToAsync()) {
await Task.Delay(100);
yield return num;
}
}
}
Обратите внимание на два изменения:
foreach
становится await foreach
nums
becoms nums.ToAsync()
Обертка настолько легка, насколько это возможно, и ее реализация основана наследующие классы (обратите внимание, что использование ValueTask<T>
в соответствии с IAsyncEnumerable<T>
и IAsyncEnumerator<T>
допускает постоянное количество выделений кучи на foreach
):
public static class EnumerableExtensions {
public static IAsyncEnumerable<T> ToAsync<T>(this IEnumerable<T> @this) => new EnumerableAdapter<T>(@this);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IAsyncEnumerator<T> ToAsync<T>(this IEnumerator<T> @this) => new EnumeratorAdapter<T>(@this);
private sealed class EnumerableAdapter<T> : IAsyncEnumerable<T> {
private readonly IEnumerable<T> target;
public EnumerableAdapter(IEnumerable<T> target) => this.target = target;
public IAsyncEnumerator<T> GetAsyncEnumerator() => this.target.GetEnumerator().ToAsync();
}
private sealed class EnumeratorAdapter<T> : IAsyncEnumerator<T> {
private readonly IEnumerator<T> enumerator;
public EnumeratorAdapter(IEnumerator<T> enumerator) => this.enumerator = enumerator;
public ValueTask<bool> MoveNextAsync() => new ValueTask<bool>(this.enumerator.MoveNext());
public T Current => this.enumerator.Current;
public ValueTask DisposeAsync() {
this.enumerator.Dispose();
return new ValueTask();
}
}
}
Подводя итог:
Чтобы иметь возможность писать методы асинхронного генератора (async IAsyncEnumerable<int> MyMethod() ...
) и использовать асинхронные перечислимые значения (await foreach (var x in ...
), просто установите NuGet в своем проекте.
Чтобы обойти преждевременную остановку итерации, убедитесь, что у вас есть System.Collections.Generic
в ваших using
предложениях, наберите .ToAsync()
на вашем IEnumerable
и включите foreach
в await foreach
.