web-dev-qa-db-ja.com

IAsyncEnumerableがC#8.0プレビューで機能しない

私は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'の定義が含まれておらず、タイプ '_IAsyncEnumerable<int>_'の最初の引数を受け入れるアクセス可能な拡張メソッド 'GetAwaiter'がありません見つけられます(usingディレクティブまたはアセンブリ参照がありませんか?)
  2. エラーCS1624「_IAsyncEnumerable<int>_」はイテレータインターフェイスタイプではないため、「Program.Get()」の本体をイテレータブロックにすることはできません

ここで何が欠けていますか?

8
Alen Alex

これはコンパイラのバグで、数行のコードを追加することで修正できます こちらにあります

_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が スピンのためにC#8を取得 で説明しているように:

しかし、コンパイルして実行しようとすると、膨大な数のエラーが発生します。それは、私たちが少し混乱して、.NET Core 3.0とVisual Studio 2019のプレビューが完全に揃っていなかったためです。具体的には、非同期イテレータが活用する実装タイプがありますが、これはコンパイラが期待するものとは異なります。

これを修正するには、プロジェクトに このブリッジコード を含む別のソースファイルを追加します。再度コンパイルすると、すべてが正常に機能するはずです。

更新

Enumerable.Range()が非同期イテレーター内で使用されている場合、別のバグがあるようです。

課題のGetNumbersAsync()メソッドは、2回の反復だけで終了します。

_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:反復はコアで早期に停止します

12

Async enumerablesを機能させるために必要なブリッジングコードに関して、数日前にそれを行う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配列に具体化されているためです。 2回の反復後に実際に終了するのは、次のように_IEnumerable<int>_自体を反復処理することです。

_var nums = Enumerable.Range(0, 10); // no more .ToArray()
foreach (var num in nums) {
_

それでも、クエリを実体化されたコレクションに変えることは賢いトリックのように思えるかもしれませんが、シーケンス全体をバッファリングしたいというわけではありません(したがって、メモリと時間の両方が失われます)。

パフォーマンスを念頭に置いて、私が見つけたのは、almostIEnumerable上のラッパーをゼロに割り当て、それがIAsyncEnumerableプラスforeachの代わりに_await foreach_を使用すると、問題を回避できます。

私は最近、NuGetパッケージの新しいバージョンを公開しました。これには、一般的に_IEnumerable<T>_のToAsync<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;
        }
    }
}
_

2つの変更に注意してください。

  • 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 をインストールするだけです。

  • 反復の早期停止を回避するために、using句に_System.Collections.Generic_が含まれていることを確認し、IEnumerable.ToAsync()を呼び出して、 foreachを_await foreach_に変換します。

0
Eduard Dumitru