关于使异步枚举工作所需的桥接代码,我在几天前发布了一个 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
成为nums.ToAsync()
包装器尽可能轻量级,它的实现基于以下类(请注意,使用ValueTask<T>
andIAsyncEnumerable<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
.