什么时候该用 IAsyncEnumerable<t></t>
而不是 IEnumerable<t></t>
当你的集合项需要逐个异步获取(比如从数据库流式读取、HTTP 分块响应、文件分片加载),且你希望在拿到前几项时就立刻开始处理,而不是等全部加载完再遍历——这时候
IAsyncEnumerable<t></t>是唯一合理选择。它不是为“加速普通循环”设计的,而是为「异步拉取 + 流式消费」场景存在的。
常见误用:把本地内存 List 包装成
IAsyncEnumerable并用
await foreach遍历——这只会增加开销,毫无收益。 适用:EF Core 6+ 的
AsAsyncEnumerable()、
HttpClient读取分块响应、自定义异步数据源 不适用:
new List<int> {1,2,3}.ToAsyncEnumerable()</int> 这类转换(除非你刻意模拟延迟)
关键信号:方法签名返回 IAsyncEnumerable<t></t>,且内部有
await(如
await reader.ReadAsync())
await foreach
的正确写法和常见崩溃点
await foreach是唯一安全消费
IAsyncEnumerable<t></t>的方式;直接调用
GetEnumerator()或尝试转成
List会丢失异步上下文或引发
InvalidOperationException。
典型错误现象:
System.InvalidOperationException: 'The collection was modified after the enumerator was instantiated.'—— 多数是因为在
await foreach循环体内又修改了同一个集合(比如边遍历边
Add),和同步
foreach一样禁止。 必须用
await foreach (var item in source),不能漏掉
await循环体内
await是允许的(比如处理每个 item 时发 HTTP 请求),但要注意整体超时控制 若需提前退出,用
break即可;
return会自动释放底层资源(如数据库连接) 不要对同一
IAsyncEnumerable实例多次
await foreach——多数实现是“一次性”的,第二次会立即完成且不返回任何项
如何手写一个简单的 IAsyncEnumerable<t></t>
数据源
不需要复杂框架,用
yield return+
async就能写。核心是返回
IAsyncEnumerable<t></t>的方法本身标记
async,并在
yield return前加
await。
注意:C# 8+ 要求目标框架支持(.NET Core 3.0+ / .NET 5+),且项目文件需启用
<langversion>8.0</langversion>或更高。
public static async IAsyncEnumerable<string> ReadLinesAsync(string path)
{
await foreach (var line in File.ReadLinesAsync(path)) // 内置支持
{
yield return line.Trim();
await Task.Delay(10); // 模拟处理延迟
}
}
yield return必须在
async IAsyncEnumerable<t></t>方法内,不能放在普通
async Task里 若要兼容旧版运行时(如 .NET Framework),需引用
System.Linq.AsyncNuGet 包,并用
AsyncEnumerable.Return等静态构造 异常传播:在
yield块中抛出的异常,会在对应
await foreach迭代时被抛出,不是在枚举器创建时
与 Task<ienumerable>></ienumerable>
的本质区别
Task<ienumerable>></ienumerable>是「异步获取整个集合」,仍是一次性加载到内存;
IAsyncEnumerable<t></t>是「异步逐个提供集合项」,内存占用恒定,适合大数据流。
性能影响明显:读取 100 万行 CSV 时,前者可能瞬间吃光几百 MB 内存并卡住 UI,后者可稳定维持几 KB 内存,每行处理完立刻释放。
Task<ienumerable>></ienumerable>:适合结果集小、后续要多次遍历、或需 LINQ 组合(如
.Where().Select())
IAsyncEnumerable<t></t>:适合单次流式处理、结果集大、或源头本身就是异步流(DB cursor、network stream) 不能混用:没有内置方法把
Task<ienumerable>></ienumerable>直接转成
IAsyncEnumerable<t></t>,强行包装会失去流式优势
真正难的不是语法,是判断数据源头是否天然支持流式拉取——比如 EF Core 查询没加
.AsAsyncEnumerable(),哪怕写了
await foreach,底层仍是先
ToList()再枚举。
