不,您目前不能将 async 与迭代器块一起使用。正如 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);
}
}
在这里,您将迭代器部分和异步部分放在不同的函数中,这允许您同时使用 async 和 yield 语法。该GetStuff
函数异步获取数据,ReadItems
然后将数据同步读取到可枚举中。
注意ToArray()
通话。像这样的东西是必要的,因为枚举器函数延迟执行,因此您的异步函数可能会在读取所有数据之前处理连接和命令。这是因为using
块覆盖了Task
执行的持续时间,但是您将对其进行迭代after
以完成任务。
此解决方案不使用,ReadAsync
但确实使用OpenAsync
and ExecuteReaderAsync
,这可能会给您带来大部分好处。以我的经验,ExecuteReader 将花费最多的时间并且最大的好处是异步。当我读完第一行时,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
立即返回一个可枚举项,其中可枚举项中的每个项目都是一个任务,SomeClass
当它完成时将呈现一个对象。这种方法有一些缺陷。首先,可枚举是同步返回的,所以在它返回时我们实际上不知道结果中有多少行,这就是为什么我将它设为无限序列的原因。这是完全合法的,但它有一些副作用。我需要使用null
表示无限任务序列中有用数据的结束。其次,你必须小心你如何迭代它。您需要向前迭代它,并且在迭代到下一行之前需要等待每一行。您还必须仅在所有任务完成后处理迭代器,以便 GC 在完成使用之前不会收集连接。由于这些原因,这不是一个安全的解决方案,我必须强调,我将其包括在内是为了帮助回答您的第二个问题。