C# IAsyncEnumerable使用方法 C#如何异步地遍历集合

12次阅读

该用 IAsyncEnumerable 而不是 IEnumerable 时:集合项需逐个异步获取且要流式消费,如数据库游标、http 分块响应;关键信号是方法返回 IAsyncEnumerable 且内部含 await。

C# IAsyncEnumerable使用方法 C#如何异步地遍历集合

什么时候该用 IAsyncEnumerable 而不是 IEnumerable

当你的集合项需要逐个异步获取(比如从数据库流式读取、HTTP 分块响应、文件分片加载),且你希望在拿到前几项时就立刻开始处理,而不是等全部加载完再遍历——这时候 IAsyncEnumerable 是唯一合理选择。它不是为“加速普通循环”设计的,而是为「异步拉取 + 流式消费」场景存在的。

常见误用:把本地内存 List 包装成 IAsyncEnumerable 并用 await foreach 遍历——这只会增加开销,毫无收益。

  • 适用:EF Core 6+ 的 AsAsyncEnumerable()HttpClient 读取分块响应、自定义异步数据源
  • 不适用:new List {1,2,3}.ToAsyncEnumerable() 这类转换(除非你刻意模拟延迟)
  • 关键信号:方法签名返回 IAsyncEnumerable,且内部有 await(如 await reader.ReadAsync()

await foreach 的正确写法和常见崩溃点

await foreach 是唯一安全消费 IAsyncEnumerable 的方式;直接调用 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 数据源

不需要复杂框架,用 yield return + async 就能写。核心是返回 IAsyncEnumerable 的方法本身标记 async,并在 yield return 前加 await

注意:C# 8+ 要求目标框架支持(.net Core 3.0+ / .NET 5+),且项目文件需启用 8.0 或更高。

public static async IAsyncEnumerable ReadLinesAsync(string path) {     await foreach (var line in File.ReadLinesAsync(path)) // 内置支持     {         yield return line.Trim();         await Task.Delay(10); // 模拟处理延迟     } }
  • yield return 必须在 async IAsyncEnumerable 方法内,不能放在普通 async Task
  • 若要兼容旧版运行时(如 .NET Framework),需引用 System.linq.Async NuGet 包,并用 AsyncEnumerable.Return 等静态构造
  • 异常传播:在 yield 块中抛出的异常,会在对应 await foreach 迭代时被抛出,不是在枚举器创建时

Task> 的本质区别

Task> 是「异步获取整个集合」,仍是一次性加载到内存;IAsyncEnumerable 是「异步逐个提供集合项」,内存占用恒定,适合大数据流。

性能影响明显:读取 100 万行 csv 时,前者可能瞬间吃光几百 MB 内存并卡住 ui,后者可稳定维持几 KB 内存,每行处理完立刻释放。

  • Task>:适合结果集小、后续要多次遍历、或需 LINQ 组合(如 .Where().select()
  • IAsyncEnumerable:适合单次流式处理、结果集大、或源头本身就是异步流(DB cursor、network stream
  • 不能混用:没有内置方法把 Task> 直接转成 IAsyncEnumerable,强行包装会失去流式优势

真正难的不是语法,是判断数据源头是否天然支持流式拉取——比如 EF Core 查询没加 .AsAsyncEnumerable(),哪怕写了 await foreach,底层仍是先 ToList() 再枚举。

text=ZqhQzanResources