什么是 IAsyncEnumerable<t></t>
,它和普通 IEnumerable<t></t>
有什么区别
IAsyncEnumerable<t></t>是 C# 8.0 引入的异步流接口,用于按需、异步地生成或消费一系列元素。它不是一次性加载全部数据(像
IEnumerable<t></t>那样可能触发同步延迟或阻塞),而是在每次
await foreach迭代时,真正等待下一个元素就绪——适合数据库游标、HTTP 流式响应、实时日志拉取等场景。
关键区别在于: -
IEnumerable<t></t>的
GetEnumerator()返回同步迭代器,
MoveNext()是同步调用; -
IAsyncEnumerable<t></t>的
GetAsyncEnumerator()返回
IAsyncEnumerator<t></t>,其
MoveNextAsync()是
ValueTask<bool></bool>,可真正异步挂起; - 必须用
await foreach消费,不能直接用
foreach(编译器会报错)。
如何定义并返回 IAsyncEnumerable<t></t>
最常用方式是用
async yield return编写本地异步迭代器方法。注意该方法必须返回
IAsyncEnumerable<t></t>,且标记为
async。
public static async IAsyncEnumerable<string> ReadLinesAsync(string path)
{
await foreach (var line in File.ReadLinesAsync(path))
{
yield return line.Trim();
}
}几点实操提醒: -
yield return在
async方法中只允许出现在
IAsyncEnumerable<t></t>或
IAsyncEnumerator<t></t>返回类型的方法里; - 不支持在
try块中
yield return(但
await可以); - 若需异常传播到消费者,直接抛出即可——
await foreach会捕获并重新抛出; - 不要手动实现
IAsyncEnumerable<t></t>,除非有特殊调度/生命周期控制需求。
await foreach
的正确写法与常见陷阱
消费端必须用
await foreach,且所在方法需标记为
async并返回
Task或
ValueTask。
public static async Task ProcessLogs()
{
await foreach (var line in ReadLinesAsync("access.log"))
{
if (line.Contains("ERROR"))
Console.WriteLine(line);
}
}容易踩的坑: - 忘记加
await:写成
foreach (var x in asyncSource)会编译失败,提示“无法隐式转换”; - 在非
async方法里调用:编译器报错 CS4032; - 混用
ConfigureAwait(false):目前
await foreach不支持直接配置上下文,若需避免上下文捕获,应在迭代器内部的
await上使用; - 提前退出循环(如
break或异常)时,
DisposeAsync()会被自动调用——但仅当迭代器实现了
IAsyncDisposable(.NET 5+ 默认支持)。
性能与兼容性注意事项
IAsyncEnumerable<t></t>在 .NET Core 3.0+ 原生支持,.NET Framework 不支持(即使装了 NuGet 包也无法获得语言级
await foreach支持)。
实际使用中要注意: - 每次
MoveNextAsync()调用都可能触发一次 await,高频小数据量场景(如内存 List 模拟)反而比同步迭代慢; - 如果底层数据源本身不支持真异步(比如包装了一个同步
IEnumerable<t></t>),那只是“假异步”,仍会阻塞线程; - LINQ 操作如
Where、
Select有对应异步扩展方法(来自
System.Linq.AsyncNuGet 包),但原生 LINQ to Objects 不支持
IAsyncEnumerable<t></t>; - 调试时注意:VS 调试器对
await foreach的断点支持良好,但内联异步 lambda 中的
yield return可能无法逐行停靠。
真正需要异步流的地方,往往涉及 IO 边界或背压控制——别为了“看起来更现代”而强行替换已有的同步集合。
