IAsyncEnumerable 在 C# 8.0 预览中不起作用

IAsyncEnumerable not working in C# 8.0 preview

我正在玩 C# 8.0 预览版,但无法 IAsyncEnumerable 工作。

我尝试了以下

public static async IAsyncEnumerable<int> Get()
{
    for(int i=0; i<10; i++)
    {
        await Task.Delay(100);
        yield return i;
    }
}

我最终使用了一个名为 AsyncEnumerator 的 Nuget 包,但我收到以下错误:

  1. 错误 CS1061“IAsyncEnumerable<int>”不包含“GetAwaiter”的定义,并且没有可访问的扩展方法“GetAwaiter”接受类型为“[=13=”的第一个参数]' 可以找到(您是否缺少 using 指令或程序集引用?)
  2. 错误 CS1624“Program.Get()”的主体不能是迭代器块,因为“IAsyncEnumerable<int>”不是迭代器接口类型

我在这里错过了什么?

这是编译器中的一个错误,可以通过添加几行代码来修复 found here :

namespace System.Threading.Tasks
{
    using System.Runtime.CompilerServices;
    using System.Threading.Tasks.Sources;

    internal struct ManualResetValueTaskSourceLogic<TResult>
    {
        private ManualResetValueTaskSourceCore<TResult> _core;
        public ManualResetValueTaskSourceLogic(IStrongBox<ManualResetValueTaskSourceLogic<TResult>> parent) : this() { }
        public short Version => _core.Version;
        public TResult GetResult(short token) => _core.GetResult(token);
        public ValueTaskSourceStatus GetStatus(short token) => _core.GetStatus(token);
        public void OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) => _core.OnCompleted(continuation, state, token, flags);
        public void Reset() => _core.Reset();
        public void SetResult(TResult result) => _core.SetResult(result);
        public void SetException(Exception error) => _core.SetException(error);
    }
}

namespace System.Runtime.CompilerServices
{
    internal interface IStrongBox<T> { ref T Value { get; } }
}

正如 Mads Torgersen 在 Take C# 8 for a spin 中解释的:

But if you try compiling and running it, you get an embarassing number of errors. That’s because we messed up a bit, and didn’t get the previews of .NET Core 3.0 and Visual Studio 2019 perfectly aligned. Specifically, there’s an implementation type that async iterators leverage that’s different from what the compiler expects.

You can fix this by adding a separate source file to your project, containing this bridging code. Compile again, and everything should work just fine.

更新

当在异步迭代器中使用 Enumerable.Range() 时,看起来还有另一个错误。

问题中的GetNumbersAsync()方法只迭代了两次就结束了:

static async Task Main(string[] args)
{
    await foreach (var num in GetNumbersAsync())
    {
        Console.WriteLine(num);
    }
}

private static async IAsyncEnumerable<int> GetNumbersAsync()
{
    var nums = Enumerable.Range(0, 10);
    foreach (var num in nums)
    {
        await Task.Delay(100);
        yield return num;
    }
}

这将只打印:

0
1

这不会发生在数组甚至其他迭代器方法中:

private static async IAsyncEnumerable<int> GetNumbersAsync()
{
    foreach (var num in counter(10))
    {
        await Task.Delay(100);
        yield return num;
    }
}

private static IEnumerable<int> counter(int count)
{
    for(int i=0;i<count;i++)
    {
        yield return i;
    }
}

这将打印预期的:

0
1
2
3
4
5
6
7
8
9

更新 2

这似乎也是一个已知错误: 异步流:Core

上的迭代提前停止

关于使异步枚举工作所需的桥接代码,我几天前发布了一个 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()

包装器尽可能轻量,其实现基于以下 类(请注意 IAsyncEnumerable<T> 和 [=31= 强制使用 ValueTask<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 在你的项目中。

  • 为了避免迭代过早停止,请确保在 using 子句中包含 System.Collections.Generic,在 [=] 上调用 .ToAsync() 18=] 并将你的 foreach 变成 await foreach.