12

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

我尝试了以下

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

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

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

我在这里想念什么?

4

2 回答 2

14

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

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中解释的那样:

但是如果你尝试编译和运行它,你会得到一个令人尴尬的错误数量。那是因为我们有点搞砸了,没有让 .NET Core 3.0 和 Visual Studio 2019 的预览完全一致。具体来说,异步迭代器利用的实现类型与编译器所期望的不同。

您可以通过将包含此桥接代码的单独源文件添加到项目中来解决此问题。再次编译,一切正常。

更新

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

似乎这也是一个已知的错误: Async-Streams:迭代在 Core 上提前停止

于 2018-12-06T12:23:31.427 回答
1

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

于 2018-12-07T22:49:13.017 回答