不,您目前无法在迭代器块中使用异步。正如svick所说,您需要类似于IAsyncEnumerable
的东西来实现这一点。
如果函数返回值是Task<IEnumerable<SomeClass>>
,那么意味着该函数返回一个单独的Task
对象,该对象一旦完成,将为您提供一个完整的IEnumerable(在此可枚举的内容不包含Task)。任务对象完成后,调用方应该能够同步地迭代在可枚举中返回的所有项。
以下是一个返回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())
{
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
执行的持续时间,但是您会在任务完成后对其进行迭代。
此解决方案不使用 ReadAsync
,但使用 OpenAsync
和 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())
{
SomeClass someClass = null;
return someClass;
}
else
return null;
}
...以及一个使用示例:
async void Caller()
{
IEnumerable<Task<SomeClass>> items = GetStuff();
foreach (Task<SomeClass> itemAsync in items)
{
SomeClass item = await itemAsync;
if (item == null)
break;
Console.WriteLine(item);
}
}
在这种情况下,
GetStuff
立即返回一个可枚举的对象,其中可枚举对象中的每个项目都是一个任务,在该任务完成后将呈现一个
SomeClass
对象。 这种方法有一些缺陷。首先,枚举对象同步返回,因此在它返回时我们实际上不知道结果中有多少行,这就是为什么我将其制作成无限序列的原因。这是完全合法的,但它具有一些副作用。 我需要使用
null
来表示无限序列中有用数据的结束。其次,您必须小心地迭代它。您需要向前迭代它,并在迭代到下一行之前等待每一行。在所有任务完成之前,您还必须仅处置迭代器,以便 GC 在使用完毕之前不收集连接。出于这些原因,这不是一个安全的解决方案,我必须强调,我包含它只是为了帮助回答您的第二个问题。
List<SomClass>
并返回它几乎可以达到相同的效果,不是吗? - svick