いいえ、現在、イテレータブロックで非同期を使用することはできません。svickが言うように、あなたはそれをするような何かが必要になるでしょうIAsyncEnumerable
。
戻り値がある場合はTask<IEnumerable<SomeClass>>
、関数が単一のTask
オブジェクトを返すことを意味します。このオブジェクトが完了すると、完全に形成されたIEnumerableが提供されます(この列挙型にはタスクの非同期の余地はありません)。タスクオブジェクトが完了すると、呼び出し元は、列挙可能オブジェクトに返されたすべてのアイテムを同期的に反復できるようになります。
これがを返すソリューションですTask<IEnumerable<SomeClass>>
。次のようなことを行うことで、非同期のメリットの大部分を得ることができます。
async Task<IEnumerable<SomeClass>> GetStuff()
{
using (SqlConnection conn = new SqlConnection(""))
{
using (SqlCommand cmd = new SqlCommand("", conn))
{
await conn.OpenAsync();
SqlDataReader reader = await cmd.ExecuteReaderAsync();
return ReadItems(reader).ToArray();
}
}
}
IEnumerable<SomeClass> ReadItems(SqlDataReader reader)
{
while (reader.Read())
{
// Create an instance of SomeClass based on row returned.
SomeClass someClass = null;
yield return someClass;
}
}
...および使用例:
async void Caller()
{
// Calls get-stuff, which returns immediately with a Task
Task<IEnumerable<SomeClass>> itemsAsync = GetStuff();
// Wait for the task to complete so we can get the items
IEnumerable<SomeClass> items = await itemsAsync;
// Iterate synchronously through the items which are all already present
foreach (SomeClass item in items)
{
Console.WriteLine(item);
}
}
ここでは、イテレータ部分と非同期部分が別々の関数にあり、非同期構文とyield構文の両方を使用できます。このGetStuff
関数は非同期でデータを取得し、ReadItems
次に同期的にデータを列挙可能に読み取ります。
ToArray()
呼び出しに注意してください。列挙子関数は遅延して実行されるため、このようなものが必要です。そうしないと、すべてのデータが読み取られる前に、非同期関数が接続とコマンドを破棄する可能性があります。これは、using
ブロックがTask
実行期間をカバーしているためですがafter
、タスクが完了したことを繰り返すことになります。
このソリューションはを使用しませんReadAsync
が、とを使用します。これにより、おそらくほとんどのメリットが得られます。私の経験では、非同期であることが最も時間がかかり、最もメリットがあるのはExecuteReaderです。最初の行を読んだ時点で、には他のすべての行がすでにあり、同期して戻ってきます。これがあなたにも当てはまる場合は、のようなプッシュベースのシステムに移行しても大きなメリットは得られません(呼び出し元の関数に大幅な変更が必要になります)。OpenAsync
ExecuteReaderAsync
SqlDataReader
ReadAsync
IObservable<T>
説明のために、同じ問題に対する別のアプローチを検討してください。
IEnumerable<Task<SomeClass>> GetStuff()
{
using (SqlConnection conn = new SqlConnection(""))
{
using (SqlCommand cmd = new SqlCommand("", conn))
{
conn.Open();
SqlDataReader reader = cmd.ExecuteReader();
while (true)
yield return ReadItem(reader);
}
}
}
async Task<SomeClass> ReadItem(SqlDataReader reader)
{
if (await reader.ReadAsync())
{
// Create an instance of SomeClass based on row returned.
SomeClass someClass = null;
return someClass;
}
else
return null; // Mark end of sequence
}
...および使用例:
async void Caller()
{
// Synchronously get a list of Tasks
IEnumerable<Task<SomeClass>> items = GetStuff();
// Iterate through the Tasks
foreach (Task<SomeClass> itemAsync in items)
{
// Wait for the task to complete. We need to wait for
// it to complete before we can know if it's the end of
// the sequence
SomeClass item = await itemAsync;
// End of sequence?
if (item == null)
break;
Console.WriteLine(item);
}
}
この場合、GetStuff
enumerableですぐに戻ります。ここで、enumerableの各アイテムは、SomeClass
完了時にオブジェクトを提示するタスクです。このアプローチにはいくつかの欠点があります。まず、列挙型は同期的に返されるため、返される時点では、結果に含まれる行数が実際にはわかりません。そのため、無限シーケンスにしました。これは完全に合法ですが、いくつかの副作用があります。私は使用する必要がありましたnull
タスクの無限のシーケンスで有用なデータの終わりを知らせるため。第二に、あなたはそれをどのように繰り返すかについて注意しなければなりません。前方に反復する必要があり、次の行に反復する前に各行を待つ必要があります。また、すべてのタスクが完了した後にのみイテレータを破棄して、GCが使用を終了する前に接続を収集しないようにする必要があります。これらの理由から、これは安全な解決策ではありません。2番目の質問に答えるために、説明のためにこれを含めていることを強調する必要があります。