私は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パッケージを使用することになりましたが、次のエラーが表示されます。
IAsyncEnumerable<int>
_'には 'GetAwaiter
'の定義が含まれておらず、タイプ '_IAsyncEnumerable<int>
_'の最初の引数を受け入れるアクセス可能な拡張メソッド 'GetAwaiter
'がありません見つけられます(usingディレクティブまたはアセンブリ参照がありませんか?)IAsyncEnumerable<int>
_」はイテレータインターフェイスタイプではないため、「Program.Get()
」の本体をイテレータブロックにすることはできませんここで何が欠けていますか?
これはコンパイラのバグで、数行のコードを追加することで修正できます こちらにあります :
_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:反復はコアで早期に停止します
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
_に変換します。