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

时间:2018-12-06 12:19:21

标签: c# c#-8.0 iasyncenumerable

我在玩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'接受类型为'IAsyncEnumerable<int>'的第一个参数可以找到(您是否缺少using指令或程序集引用?)
  2. 错误CS1624'Program.Get()'的主体不能是迭代器块,因为'IAsyncEnumerable<int>'不是迭代器接口类型

我在这里想念什么?

2 个答案:

答案 0 :(得分:9)

这是编译器中的一个错误,可以通过添加几行代码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中所述:

  

但是,如果您尝试编译和运行它,则会得到令人尴尬的错误。那是因为我们搞砸了一点,但没有使.NET Core 3.0和Visual Studio 2019的预览完美对齐。具体而言,有一种实现类型可以利用异步迭代器来实现,与编译器的期望有所不同。

     

您可以通过将一个单独的源文件添加到项目中来解决此问题,该文件包含this bridging code。再次编译,一切都应该正常工作。

更新

在异步迭代器中使用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: iteration stops early on Core

答案 1 :(得分:0)

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