メモ
この記事は機能仕様についてです。 仕様は、機能の設計ドキュメントとして使用できます。 これには、提案された仕様の変更および機能の設計と開発時に必要な情報が含まれます。 これらの記事は、提案された仕様の変更が決定され、現在の ECMA 仕様に組み込まれるまで公開されます。
機能の仕様と行われた実装では、いくつかの違いがあることがあります。 これらの違いは、関連する言語設計ミーティング (LDM) メモに取り上げられています。
機能仕様を C# 言語標準に導入するプロセスの詳細については、仕様に関する記事を参照してください。
チャンピオンの課題: https://github.com/dotnet/csharplang/issues/43
まとめ
C# では反復子メソッドと非同期メソッドがサポートされていますが、反復子と非同期メソッドの両方であるメソッドはサポートされません。 これを修正するには、await
を新しい形式の async
反復子として使用し、IAsyncEnumerable<T>
や IAsyncEnumerator<T>
の代わりに IEnumerable<T>
または IEnumerator<T>
を返す形にし、新しい IAsyncEnumerable<T>
で await foreach
を使用できるようにする必要があります。 IAsyncDisposable
インターフェイスは、非同期クリーンアップを有効にするためにも使用されます。
関連するディスカッション
詳細な設計
インターフェイス
IAsyncDisposable
IAsyncDisposable
(https://github.com/dotnet/roslyn/issues/114など) と、それが良いアイデアかどうかについて多くの議論が行われています。 ただしこれは、非同期反復子のサポートを追加するために必要な概念です。 finally
ブロックには await
が含まれている可能性があるため、finally
ブロックは反復子の破棄の一部として実行する必要があるため、非同期破棄が必要です。 リソースのクリーンアップに時間がかかる可能性があるときに、たとえば、ファイルを閉じる(フラッシュが必要)、コールバックの登録解除や、登録解除が完了したことを知る方法を提供する場合など、一般的に役立ちます。
次のインターフェイスがコア .NET ライブラリ (System.Private.CoreLib/System.Runtime など) に追加されます。
namespace System
{
public interface IAsyncDisposable
{
ValueTask DisposeAsync();
}
}
Dispose
と同様に、DisposeAsync
を複数回呼び出しても問題はありません。1 回目以降の後続の呼び出しは操作なしとして扱う必要があります。同期的に完了した成功したタスクを返します (DisposeAsync
は、スレッド セーフである必要はなく、同時呼び出しをサポートする必要もありません)。 さらに、型は IDisposable
と IAsyncDisposable
の両方を実装できます。その場合、Dispose
を呼び出してから DisposeAsync
またはその逆の順番で呼び出すことが可能ですが、最初の呼び出しだけが意味があり、それ以降の呼び出しのみが nop である必要があります。 そのため、型が両方を実装する場合、コンシューマーは、コンテキスト、同期コンテキストの Dispose
、非同期コンテキストの DisposeAsync
に基づいてより関連性の高いメソッドを 1 回だけ呼び出すことが推奨されます。
(IAsyncDisposable
が using
と通信する方法については別途説明します。また、foreach
との通信方法については、この提案の後半で説明します)。
考慮すべき選択肢
-
CancellationToken
を許容するDisposeAsync
: 論理的には、非同期的なものは取り消すことができ、破棄はクリーンアップ、終了、リソースの解放などであり、これらは一般的に取り消すべきものではありません。クリーンアップは、取り消される作業にとって依然として重要です。 実際の作業を取り消す原因となったのと同じCancellationToken
は、通常、DisposeAsync
に渡されるトークンと同じで、作業の取り消しによってDisposeAsync
が no-opになるため、DisposeAsync
の価値が無くなります。 破棄を待機するブロックを回避したい場合は、結果として得られるValueTask
を待つのを避けるか、一定期間だけ待機することができます。 -
Task
を返すDisposeAsync
: 現在は、非ジェネリックValueTask
が存在し、IValueTaskSource
から構築できるため、DisposeAsync
からValueTask
を返すと、既存のオブジェクトを、最終的なDisposeAsync
の非同期完了を表す保証として再利用でき、DisposeAsync
が非同期で完了するケースのTask
割り当てが保存されます。 -
bool continueOnCapturedContext
(ConfigureAwait
) を使用してDisposeAsync
を構成する: このようなコンセプトがどのようにusing
、foreach
およびこれを消費する別の言語に公開されるかに関連する問題があり、インターフェイスの観点からは、実際には、await
を実行していないため、構成をするものはありません。ValueTask
の消費者は、いつでもそれを消費できます。 -
IDisposable
を継承するIAsyncDisposable
: いずれか一方のみを使用する必要があるため、型に両方を強制的に実装することは意味がありません。 -
IAsyncDisposable
ではなくIDisposableAsync
: モノ/型は "非同期の何か" という名前に従ってきましたが、操作は "done async" であるため、型はプレフィックスとして "Async" を持ち、メソッドはサフィックスとして "Async" を持ちます。
IAsyncEnumerable / IAsyncEnumerator
コア .NET ライブラリには、次の 2 つのインターフェイスが追加されます。
namespace System.Collections.Generic
{
public interface IAsyncEnumerable<out T>
{
IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default);
}
public interface IAsyncEnumerator<out T> : IAsyncDisposable
{
ValueTask<bool> MoveNextAsync();
T Current { get; }
}
}
(追加の言語機能を使用しない) 一般的な消費量は次のようになります。
IAsyncEnumerator<T> enumerator = enumerable.GetAsyncEnumerator();
try
{
while (await enumerator.MoveNextAsync())
{
Use(enumerator.Current);
}
}
finally { await enumerator.DisposeAsync(); }
検討されたが破棄されたオプション。
-
Task<bool> MoveNextAsync(); T current { get; }
:Task<bool>
を使用すると、キャッシュされたタスク オブジェクトを使用して同期的に成功したMoveNextAsync
の呼び出しを表すことがサポートされますが、非同期の完了には以前として割り当てが必要です。ValueTask<bool>
を返すことで、列挙子オブジェクト自体がIValueTaskSource<bool>
を実装し、MoveNextAsync
から返されるValueTask<bool>
のバックアップとして使用できます。これにより、オーバーヘッドが大幅に削減できます。 -
ValueTask<(bool, T)> MoveNextAsync();
: 消費するのが難しいだけでなく、T
が共変ではなくなったことを意味します。 -
ValueTask<T?> TryMoveNextAsync();
: 共変ではありません。 -
Task<T?> TryMoveNextAsync();
: 共変ではなく、すべての呼び出しに対する割り当てなど -
ITask<T?> TryMoveNextAsync();
: 共変ではなく、すべての呼び出しに対する割り当てなど -
ITask<(bool,T)> TryMoveNextAsync();
: 共変ではなく、すべての呼び出しに対する割り当てなど Task<bool> TryMoveNextAsync(out T result);
:out
の結果は、操作が非同期的にタスクを完了したときではなく、同期的に戻されたときに設定する必要があります。そのため、タスクが将来のいつ完了するか分からない場合、結果を伝達する方法はありません。-
IAsyncDisposable
を実装していないIAsyncEnumerator<T>
: これらを分離することができます。 ただし、これを行うと、コードが列挙子が破棄を提供しない可能性に対処できる必要があるため、提案の他の特定の領域が複雑になり、パターンベースのヘルパーを記述することが困難になります。 さらに、列挙子には破棄の必要性 (たとえば、finally ブロックを持つ C# 非同期反復子、ネットワーク接続からのデータを列挙するものなど) が必要になるのが一般的です。そうでない場合は、追加のオーバーヘッドを最小限に抑えてpublic ValueTask DisposeAsync() => default(ValueTask);
としてメソッドを簡単に実装できます。 - _
IAsyncEnumerator<T> GetAsyncEnumerator()
: cancellation token パラメーターがありません。
次のサブセクションでは、選択されなかった代替方法について説明します。
実行可能な代替手段:
namespace System.Collections.Generic
{
public interface IAsyncEnumerable<out T>
{
IAsyncEnumerator<T> GetAsyncEnumerator();
}
public interface IAsyncEnumerator<out T> : IAsyncDisposable
{
ValueTask<bool> WaitForNextAsync();
T TryGetNext(out bool success);
}
}
TryGetNext
は、同期的に使用できる限り、1 つのインターフェイス呼び出しで項目を使用するために内部ループで使用されます。 次の項目を同期的に取得できない場合、false を返し、false を返すたびに、呼び出し元は、次の項目が使用可能になるのを待つか、別の項目が存在しないことを判断するために、WaitForNextAsync
を呼び出す必要があります。 (追加の言語機能を使用しない) 一般的な消費量は次のようになります。
IAsyncEnumerator<T> enumerator = enumerable.GetAsyncEnumerator();
try
{
while (await enumerator.WaitForNextAsync())
{
while (true)
{
int item = enumerator.TryGetNext(out bool success);
if (!success) break;
Use(item);
}
}
}
finally { await enumerator.DisposeAsync(); }
この利点は2つあり、小さいものと大きいものがあります。
- Minor: 列挙子が複数のコンシューマー をサポートできるようにします。 列挙子が複数の同時実行コンシューマーをサポートすることが重要なシナリオがある場合があります。 これは、
MoveNextAsync
とCurrent
が分離されており、その使い方をアトミックにできない実装では実現できません。 これに対し、この方法では、列挙子を前方にプッシュして次の項目を取得することをサポートする 1 つのメソッドTryGetNext
が提供されるため、必要に応じて列挙子がアトミック性を有効にすることができます。 ただし、このようなシナリオは、共有列挙可能な列挙子から各コンシューマーに独自の列挙子を与えることで有効にできる場合があります。 さらに、すべての列挙子が同時使用をサポートするように強制することは望ましくありません。これは、それを必要としない大多数のケースに自明ではないオーバーヘッドを追加するためです。つまり、インターフェイスのコンシューマーは一般的にこれに依存できません。 - 主要: パフォーマンス.
MoveNextAsync
/Current
アプローチでは、操作ごとに 2 つのインターフェイス呼び出しが必要ですが、WaitForNextAsync
/TryGetNext
の最善のケースでは、ほとんどのイテレーションが同期的に完了し、TryGetNext
で厳しいインナーループを有効にします。このため、各操作で呼び出すインターフェイスは 1 つのみです。 これは、インターフェイス呼び出しが計算を支配する状況で測定可能な影響を与える可能性があります。
ただし、これらを手動で扱う場合の複雑さが著しく増し、それに伴いバグを発生させる可能性が高くなるなど、注意が必要な欠点があります。 また、パフォーマンス上の利点は、マイクロベンチマークに現れますが、実際の使用の大部分で影響を受けるとは考えていません。 それが判明した場合は、2 つ目のインターフェイス セットを導入できます。
検討されたが破棄されたオプション。
-
ValueTask<bool> WaitForNextAsync(); bool TryGetNext(out T result);
:out
パラメーターを共変にすることはできません。 参照型の結果に対してランタイム書き込みバリアが発生する可能性が高いという小さな影響もあります(これは try パターン全体に共通する問題です)。
キャンセル
キャンセルをサポートするには、いくつかの方法があります。
-
IAsyncEnumerable<T>
/IAsyncEnumerator<T>
はキャンセルに依存しません。CancellationToken
はどこにも表示されません。 キャンセルは、反復子を呼び出すとき、CancellationToken
を反復子メソッドに引数として渡し、他のパラメーターと同様に反復子の本文で使用するなど、適切な方法でCancellationToken
を列挙可能な列挙子に論理的にベイクすることで実現されます。 IAsyncEnumerator<T>.GetAsyncEnumerator(CancellationToken)
:CancellationToken
をGetAsyncEnumerator
に渡すと、後続のMoveNextAsync
操作は可能な限りそれを考慮します。-
IAsyncEnumerator<T>.MoveNextAsync(CancellationToken)
:CancellationToken
を個別のMoveNextAsync
呼び出しに渡します。 - 1 && 2:
CancellationToken
を列挙可能型または列挙子に埋め込み、CancellationToken
をGetAsyncEnumerator
に渡します。 - 1 && 3:
CancellationToken
を列挙可能/列挙子に埋め込み、CancellationToken
をMoveNextAsync
に渡します。
純粋に理論的な観点から見ると、(5) は最も堅牢です。(a) CancellationToken
を許可する MoveNextAsync
は、キャンセルされた内容を最もきめ細かく制御でき、(b) CancellationToken
は、任意の型に埋め込まれた反復子に引数として渡すことができる他の型にすぎません。
ただし、このアプローチにはいくつか問題があります。
GetAsyncEnumerator
に渡されたCancellationToken
はどのように反復子の本文に変換されるのでしょうか?iterator
に渡されたCancellationToken
にアクセスするために新しいGetEnumerator
キーワードを導入し、それを使ってアクセスすることも可能ですが、a) それには多くの追加の処理が必要であり、b) これを非常に優先度の高い要素としています。c) 99% ケースで、イテレーターを呼び出し、かつそれにGetAsyncEnumerator
を呼び出すための同一のコードに見えるため、その場合はCancellationToken
をメソッドの引数として渡すことができます。MoveNextAsync
に渡されたCancellationToken
はどのようにメソッドの本文に挿入されるのでしょうか? これはさらに悪いことに、iterator
ローカル オブジェクトから公開されているかのように、その値は await 間で変更される可能性があります。つまり、トークンに登録されたコードは、待機する前に登録を解除してから、後で再登録する必要があります。また、反復子でコンパイラによって実装されているか、開発者によって手動で実装されているかに関係なく、すべてのMoveNextAsync
呼び出しでこのような登録と登録解除を行う必要がある場合は、非常にコストがかかる可能性があります。- 開発者は
foreach
どのようにループを取り消すのでしょうか? 列挙可能/列挙子にCancellationToken
を与えることによって行われる場合は、その後、a)foreach
をサポートする必要があります。これにより、これを非常に優先度の高い要素となり、列挙子 (LINQ メソッドなど) に基づいて構築されたエコシステムについて考え始める必要があります。b) 提供されたトークンを格納するIAsyncEnumerable<T>
のWithCancellation
拡張メソッドを使用して、列挙型にCancellationToken
を埋め込む必要があります。返された構造体のGetAsyncEnumerator
が呼び出されたときに、ラップされた列挙可能なGetAsyncEnumerator
に渡します (そのトークンは無視されます)。 または、foreach の本文に含まれるCancellationToken
だけを使用できます。 - クエリの理解がサポートされる場合またはされる時に、
CancellationToken
をGetEnumerator
やMoveNextAsync
に指定して、各句に渡すにはどうすればよいでしょうか。 最も簡単な方法は、句がそれをキャプチャすることで、その結果、GetAsyncEnumerator
/MoveNextAsync
に渡されるトークンは無視されることになります。
このドキュメントの以前のバージョンでは (1) をお勧めしますが、(4) に切り替えました。
(1) の 2 つの主な問題:
- 取り消し可能な列挙体のプロデューサーは、いくつかの定型句を実装する必要があり、非同期反復子に対するコンパイラのサポートを利用して
IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken)
メソッドを実装することしかできません。 - 多くのプロデューサーは、代わりに非同期列挙可能なシグネチャに
CancellationToken
パラメーターを追加する場合があります。これにより、コンシューマーがIAsyncEnumerable
型を指定したときに必要なキャンセル トークンを渡すことができなくなります。
この場合、主に 2 つの消費シナリオが考えられます。
- コンシューマーが非同期イテレーター メソッドを呼び出す
await foreach (var i in GetData(token)) ...
- コンシューマーが特定の
await foreach (var i in givenIAsyncEnumerable.WithCancellation(token)) ...
インスタンスを扱う場所がIAsyncEnumerable
です。
非同期ストリームのプロデューサーとコンシューマーの両方にとって便利な方法で、両方のシナリオをサポートする妥当な妥協点は、非同期反復子メソッドで特別に注釈付けされたパラメーターを使用することです。 この目的には、[EnumeratorCancellation]
属性が使用されます。 この属性をパラメーターに配置すると、トークンが GetAsyncEnumerator
メソッドに渡された場合は、パラメーターに最初に渡された値ではなく、そのトークンを使用する必要があることをコンパイラに指示します。
IAsyncEnumerable<int> GetData([EnumeratorCancellation] CancellationToken token = default)
を検討します。
このメソッドの実装者は、メソッド本文で単純にパラメーターを使用できます。
コンシューマーは、上記のいずれかの消費パターンを使用できます。
GetData(token)
を使用する場合、トークンは非同期列挙可能に保存され、イテレーションで使用されます。givenIAsyncEnumerable.WithCancellation(token)
を使用する場合、GetAsyncEnumerator
に渡されたトークンは、非同期列挙可能に保存されたトークンよりも優先されます。
foreach
foreach
は、IEnumerable<T>
の既存のサポートに加えて、IAsyncEnumerable<T>
をサポートするように拡張されます。 関連するメンバーがパブリックに公開されている場合、パターンとして IAsyncEnumerable<T>
に相当するものをサポートします。公開されていない場合は、インターフェイスを直接使用します。これにより、割り当てを回避し、MoveNextAsync
および DisposeAsync
を戻り値型として代替の待機可能型を使用するできる構造体ベースの拡張を有効にします。
構文
使用する構文:
foreach (var i in enumerable)
C# は enumerable
を同期列挙可能として引き続き扱います。したがって、非同期列挙体に関連する API が公開されている場合でも (パターンを公開したり、インターフェイスを実装したりする)、synchronous API のみが考慮されます。
foreach
が非同期 API のみを考慮させるために、await
は次のように挿入します。
await foreach (var i in enumerable)
async API または sync API のいずれかを使用してサポートする構文は提供されていません。開発者は、使用する構文に基づいて選択する必要があります。
Semantics
await foreach
ステートメントのコンパイル時の処理では、まず、式の コレクション型、列挙子型および反復型 を決定します (https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/statements.md#1395-the-foreach-statement とよく似ています)。 この決定は次のように進みます。
- 式の
X
型がdynamic
または配列型の場合。エラーが表示され、それ以上の手順は実行されません。 - それ以外の場合は、型
X
に適切なGetAsyncEnumerator
メソッドがあるかどうかを判断します。- 識別子
GetAsyncEnumerator
を持ち、型引数を持たない型X
に対してメンバー参照を実行します。 メンバー参照で一致が生成されない場合、またはあいまいさが生成される場合、あるいはメソッド グループではない一致が生成される場合は、次に説明するように列挙可能なインターフェイスを確認します。 - 最終的なメソッド グループと空の引数リストを使用して、オーバーロードの解決を実行します。 オーバーロードの解決によって該当するメソッドが存在しない場合、あいまいになる場合、または単一の最適なメソッドになるが、そのメソッドが静的であるかパブリックでない場合は、次に説明するように列挙可能なインターフェイスを確認します。
GetAsyncEnumerator
メソッドの戻り値の型E
がクラス、構造体、またはインターフェイス型でない場合は、エラーが生成され、それ以上の手順は実行されません。- 識別子
Current
を持ち、型引数を持たないE
に対してメンバー参照を実行します。 メンバー参照で一致が生成されない場合、結果がエラーである場合、または読み取りを許可するパブリック インスタンス プロパティ以外の結果である場合は、エラーが生成され、それ以上の手順は実行されません。 - 識別子
MoveNextAsync
を持ち、型引数を持たないE
に対してメンバー参照を実行します。 メンバー参照で一致が生成されない場合、結果がエラーである場合、または結果がメソッド グループを除くものである場合、エラーが生成され、それ以上の手順は実行されません。 - オーバーロードの解決は、空の引数リストを使用してメソッド グループに対して実行されます。 オーバーロードの解決の結果、適用可能なメソッドが得られない場合、あいまいさが生じる場合、または 1 つの最適なメソッドになるものの、そのメソッドが静的であるかパブリックでない場合、あるいはその戻り値の型が
bool
で awaitable はない場合、エラーが生成され、それ以上の手順は実行されません。 - コレクション型は、
X
で、列挙子型は、E
で、イテレーション型はCurrent
プロパティの型です。
- 識別子
- それ以外の場合は、列挙可能なインターフェイスを確認します。
X
からIAsyncEnumerable<ᵢ>
に暗黙的に変換されるすべての型Tᵢ
に、固有の型T
があり、T
が動的ではなく、他のすべてのTᵢ
に対して、IAsyncEnumerable<T>
からIAsyncEnumerable<Tᵢ>
への暗黙的変換がある場合、コレクション型は、インターフェイスIAsyncEnumerable<T>
になり。列挙子型は、インターフェイスIAsyncEnumerator<T>
になり、イテレーション型は、T
になります。- それ以外では、このような型
T
が複数存在する場合 、エラーが生成され、それ以上の手順は実行されません。
- それ以外の場合はエラーが生成され、それ以上の手順は実行されません。
上記の手順が成功した場合は、コレクション型 C
、列挙子型 E
、イテレーション型 T
を明確に生成します。
await foreach (V v in x) «embedded_statement»
は次のように展開されます。
{
E e = ((C)(x)).GetAsyncEnumerator();
try {
while (await e.MoveNextAsync()) {
V v = (V)(T)e.Current;
«embedded_statement»
}
}
finally {
... // Dispose e
}
}
finally
ブロックの本文んは、次の手順に基づいて構築されます。
- 型
E
に適切なDisposeAsync
メソッドがある場合:- 識別子
DisposeAsync
を持ち、型引数を持たない型E
に対してメンバー参照を実行します。 メンバー参照で一致が生成されない場合、またはあいまいさが生成される場合、あるいはメソッド グループではない一致が生成される場合は、次に説明するように破棄可能なインターフェイスを確認します。 - 最終的なメソッド グループと空の引数リストを使用して、オーバーロードの解決を実行します。 オーバーロードの解決によって該当するメソッドが存在しない場合、あいまいになる場合、または単一の最適なメソッドになるが、そのメソッドが静的であるかパブリックでない場合は、次に説明するように破棄可能なインターフェイスを確認します。
DisposeAsync
メソッドの戻り値型を待機できない場合は、エラーが生成され、それ以上の手順は実行されません。finally
句は、次の意味に相当する形に拡張されます。
finally { await e.DisposeAsync(); }
- 識別子
- それ以外の場合、
E
からSystem.IAsyncDisposable
インターフェイスへの暗黙的な変換がある場合は、E
が、null 許容値型以外の場合、finally
句は、次のセマンティックに相当するものに展開されます。
finally { await ((System.IAsyncDisposable)e).DisposeAsync(); }
- それ以外の場合、
finally
句は次のセマンティックに相当するものに拡張されます。
ただし、finally { System.IAsyncDisposable d = e as System.IAsyncDisposable; if (d != null) await d.DisposeAsync(); }
E
が値型または値型にインスタンス化された型パラメーターである場合、e
からSystem.IAsyncDisposable
への変換ではボックス化は行われません。
- それ以外の場合、
finally
句は空のブロックに展開されます。finally { }
ConfigureAwait
このパターンベースのコンパイルでは、ConfigureAwait
拡張メソッドを使用して、すべての await で ConfigureAwait
を使用できます。
await foreach (T item in enumerable.ConfigureAwait(false))
{
...
}
これもまた .NET に追加される型に基づいて行われ、System.Threading.Tasks.Extensions.dllになる可能性があります。
// Approximate implementation, omitting arg validation and the like
namespace System.Threading.Tasks
{
public static class AsyncEnumerableExtensions
{
public static ConfiguredAsyncEnumerable<T> ConfigureAwait<T>(this IAsyncEnumerable<T> enumerable, bool continueOnCapturedContext) =>
new ConfiguredAsyncEnumerable<T>(enumerable, continueOnCapturedContext);
public struct ConfiguredAsyncEnumerable<T>
{
private readonly IAsyncEnumerable<T> _enumerable;
private readonly bool _continueOnCapturedContext;
internal ConfiguredAsyncEnumerable(IAsyncEnumerable<T> enumerable, bool continueOnCapturedContext)
{
_enumerable = enumerable;
_continueOnCapturedContext = continueOnCapturedContext;
}
public ConfiguredAsyncEnumerator<T> GetAsyncEnumerator() =>
new ConfiguredAsyncEnumerator<T>(_enumerable.GetAsyncEnumerator(), _continueOnCapturedContext);
public struct ConfiguredAsyncEnumerator<T>
{
private readonly IAsyncEnumerator<T> _enumerator;
private readonly bool _continueOnCapturedContext;
internal ConfiguredAsyncEnumerator(IAsyncEnumerator<T> enumerator, bool continueOnCapturedContext)
{
_enumerator = enumerator;
_continueOnCapturedContext = continueOnCapturedContext;
}
public ConfiguredValueTaskAwaitable<bool> MoveNextAsync() =>
_enumerator.MoveNextAsync().ConfigureAwait(_continueOnCapturedContext);
public T Current => _enumerator.Current;
public ConfiguredValueTaskAwaitable DisposeAsync() =>
_enumerator.DisposeAsync().ConfigureAwait(_continueOnCapturedContext);
}
}
}
}
このアプローチでは、パターンベースの列挙子で ConfigureAwait
を使用することはできませんが、ConfigureAwait
が Task
/Task<T>
/ValueTask
/ValueTask<T>
の拡張機能としてのみ公開され、任意の待機可能なものに適用できないことに注意してください (タスクの継続サポートに実装された動作を制御します)。したがって、待機可能なものがタスクではない可能性があるパターンを使用する場合は意味がありません。 待機可能なものを返すユーザーは、このような高度なシナリオで独自のカスタム動作を提供できます。
(スコープ レベルまたはアセンブリ レベルの ConfigureAwait
ソリューションをサポートする何らかの方法がある場合、これは必要ありません)。
非同期反復子
言語/コンパイラは、IAsyncEnumerable<T>
と IAsyncEnumerator<T>
の生成と消費をサポートします。 現在、言語は次のような反復子の記述をサポートしています。
static IEnumerable<int> MyIterator()
{
try
{
for (int i = 0; i < 100; i++)
{
Thread.Sleep(1000);
yield return i;
}
}
finally
{
Thread.Sleep(200);
Console.WriteLine("finally");
}
}
ただし、await
これらの反復子の本文では使用できません。 そのサポートを追加します。
構文
反復子に対する既存の言語サポートは、yield
が含まれているかどうかに基づいて、メソッドの反復子の性質を推論します。 非同期反復子についても同じことが当てはまります。 このような非同期反復子は、シグネチャに async
を追加することで同期反復子と区別され、戻り値の型として IAsyncEnumerable<T>
または IAsyncEnumerator<T>
も必要です。 たとえば、上記の例は、非同期反復子として次のように記述できます。
static async IAsyncEnumerable<int> MyIterator()
{
try
{
for (int i = 0; i < 100; i++)
{
await Task.Delay(1000);
yield return i;
}
}
finally
{
await Task.Delay(200);
Console.WriteLine("finally");
}
}
考慮すべき選択肢
- 署名 で
async
を使用しない:async
の使用は、コンパイラによって技術的に必要になる可能性があります。これは、そのコンテキストでawait
が有効かどうかを判断するために使用します。 ただし、必要ない場合でも、await
はasync
としてマークされたメソッドでのみ使用できることは確立されており、一貫性を保つことが重要であると思われます。 -
IAsyncEnumerable<T>
のカスタム ビルダーを有効にする: これは将来を見据えるものですが、機会は複雑で、同期対応するユーザーにはサポートされていません。 -
iterator
キーワードを で保持する: 非同期反復子は、シグネチャでasync iterator
を使用し、yield
は、iterator
を含むasync
メソッドのみで使用されます。iterator
は、同期反復子ではオプションです。 自分の視点に応じて、コードがyield
を使用しているかどうかに基づいてコンパイラが型を作成するのではなく、yield
が許可されているかどうか、メソッドが実際、型IAsyncEnumerable<T>
のインスタンスを返すことを意図しているかどうかのシグネチャによって非常に明確にするメリットがあります。 しかし、同期イテレーターはこれを必要とせず、それを必要とするようにすることはできません。 さらに、一部の開発者は余分な構文を好みません。 ゼロから設計する場合は、おそらくこれを必須にしますが、この時点で非同期反復子を同期反復子の近くに保つ方がはるかに多くのメリットがあります。
LINQ
System.Linq.Enumerable
クラスには、200 以上のメソッドのオーバーロードがあり、そのすべてが、IEnumerable<T>
の観点から機能します。これらの一部は、IEnumerable<T>
を許可し、そのうちのいくつかは、IEnumerable<T>
を生成し、多くは両方を行います。 IAsyncEnumerable<T>
に LINQ サポートを追加するには、IAsyncEnumerable<T>
用にこれらのオーバーロードをすべて複製し、さらに約 200 個のオーバーロードを用意する必要があります。 また、IAsyncEnumerator<T>
はIEnumerator<T>
よりも非同期世界のスタンドアロン エンティティとして、より一般的である可能性が高いため、IAsyncEnumerator<T>
で動作する別の 200 のオーバーロードが必要になる場合があります。 さらに、多くのオーバーロードは述語 (たとえば、Where
が Func<T, bool>
を受け取るなど) を扱います。また、同期述語と非同期述語の両方を処理する IAsyncEnumerable<T>
ベースのオーバーロード (たとえば、Func<T, bool>
に加えて Func<T, ValueTask<bool>>
) を使用することが望ましい場合があります。 これは、現在の約 400 個の新しいオーバーロードすべてに適用できるわけではありませんが、大まかな計算では、もう 1 つの約 200 個のオーバーロードを意味する半分に適用され、合計で約 600 個の新しいメソッドが適用されます。
これは膨大な数の API であり、Interactive Extensions (Ix) などの拡張ライブラリが考慮される場合は、さらに多くの可能性があります。 しかし、Ix はすでにこれらの多くの実装を保持しており、その作業を複製する大きな理由はないようです。代わりに、コミュニティで Ix の改善を支援し、開発者が IAsyncEnumerable<T>
を使用して LINQ を使用する場合に推奨する必要があります。
クエリ理解構文の問題もあります。 クエリ理解のパターンベースの性質により、Ix が次のメソッドを提供する場合など、一部の演算子で "単純に動作" できるようになります。
public static IAsyncEnumerable<TResult> Select<TSource, TResult>(this IAsyncEnumerable<TSource> source, Func<TSource, TResult> func);
public static IAsyncEnumerable<T> Where(this IAsyncEnumerable<T> source, Func<T, bool> func);
その後、この C# コードは "単純に動作" します。
IAsyncEnumerable<int> enumerable = ...;
IAsyncEnumerable<int> result = from item in enumerable
where item % 2 == 0
select item * 2;
ただし、句での await
の使用をサポートするクエリ理解構文はありません。Ix が追加された場合は、次のようになります。
public static IAsyncEnumerable<TResult> Select<TSource, TResult>(this IAsyncEnumerable<TSource> source, Func<TSource, ValueTask<TResult>> func);
その後、これは問題なく動くでしょう。
IAsyncEnumerable<string> result = from url in urls
where item % 2 == 0
select SomeAsyncMethod(item);
async ValueTask<int> SomeAsyncMethod(int item)
{
await Task.Yield();
return item * 2;
}
しかし、await
句に select
をインラインで記述する方法はありません。 別の取り組みとして、async { ... }
式を言語に追加することを調べることができ、その時点でクエリの理解で使用できるようにすることができ、代わりに、上記は次のように記述できます。
IAsyncEnumerable<int> result = from item in enumerable
where item % 2 == 0
select async
{
await Task.Yield();
return item * 2;
};
または、async from
をサポートするなどして、式で直接使用するように await
を有効にします。 ただし、ここでの設計が機能一式の残りの部分に影響を与える可能性は低く、これは現在投資する価値が特に高いものではありません。そのため、今ここで追加の操作を行う必要はありません。
他の非同期フレームワークとの統合
IObservable<T>
やその他非同期フレームワーク (例えばリアクティブ ストリーム) との統合は、言語レベルではなくライブラリ レベルで行われます。 たとえば、列挙子に IAsyncEnumerator<T>
し、データをオブザーバーに IObserver<T>
するだけで、await foreach
のすべてのデータを OnNext
に公開できるため、AsObservable<T>
拡張メソッドを使用することが可能になります。 IObservable<T>
で await foreach
を消費するには、前の項目がまだ処理中のときに別の項目がプッシュされた場合に備えてデータをバッファリングする必要がありますが、そのようなプッシュプルアダプターを簡単に実装することで、IObservable<T>
を使用して IAsyncEnumerator<T>
を取得することができます。 Rx/Ix は既にそのような実装のプロトタイプを提供しており、https://github.com/dotnet/corefx/tree/master/src/System.Threading.Channels のようなライブラリはさまざまな種類のバッファリング データ構造を提供しています。 この段階では、言語を使用する必要はありません。
C# feature specifications