yield return 是同步迭代器,不能直接用于 async 方法
在 C# 中,
yield return生成的是
IEnumerable<t></t>或
IEnumerator<t></t>,整个迭代过程必须是同步的。如果你在
async方法里写
yield return,编译器会报错:
error CS1983: The return type of an async method must be void, Task, Task<t>, or a task-like type</t>。这是因为
yield return要求方法返回
IEnumerable<t></t>,而该类型不满足 async 方法的返回类型约束。
常见错误写法:
public async IEnumerable<int> GetNumbersAsync() // ❌ 编译失败
{
await Task.Delay(100);
yield return 1;
}解决思路只有两个:要么去掉
async(纯同步),要么换用 async stream。
async stream 用 IAsyncEnumerable + await foreach + yield return
C# 8.0 引入的 async stream 是专为“异步产生序列”设计的机制,底层基于
IAsyncEnumerable<t></t>和
IAsyncEnumerator<t></t>。它允许你在方法中既写
await,又写
yield return,但前提是方法签名必须是
IAsyncEnumerable<t></t>,且标记
async。
正确写法示例:
public async IAsyncEnumerable<int> GetNumbersAsync()
{
await Task.Delay(100);
yield return 1;
await Task.Delay(100);
yield return 2;
}调用时必须用
await foreach:
await foreach (var n in GetNumbersAsync())
{
Console.WriteLine(n); // 输出 1,然后 2,中间各延迟 100ms
}关键点:
IAsyncEnumerable<t></t>不是
IEnumerable<t></t>的“异步版接口”,二者无继承关系 不能把
IAsyncEnumerable<t></t>当作
IEnumerable<t></t>直接传给老代码,会编译失败 每次
yield return后可以
await任意异步操作,包括 I/O、数据库查询、HTTP 请求等
性能和执行时机差异明显
yield return是“拉取式(pull-based)”:调用者控制何时取下一个元素(比如
foreach每次迭代才触发一次
MoveNext),整个过程同步阻塞,但延迟执行(lazy)。
IAsyncEnumerable<t></t>是“异步拉取式”:每次
await foreach迭代时,内部会
await MoveNextAsync(),所以每个
yield return之间可真正挂起线程、释放上下文,适合高延迟或大量并发数据流场景(如实时日志、EventHub 消息流、分页 API 流式响应)。
容易踩的坑:
误以为IAsyncEnumerable<t></t>会自动并行执行所有
yield return前的
await—— 实际仍是串行,一个完成才走下一个 在 ASP.NET Core 中返回
IAsyncEnumerable<t></t>给 MVC 控制器时,需确保使用 .NET 5+ 且配置了
EnableRangeProcessing等支持(否则可能缓冲全部结果) 未处理取消:推荐加上
CancellationToken参数,并在
await调用中传递,否则无法响应客户端中断
两者不能混用,但可以桥接(需谨慎)
没有隐式转换,也不能直接
return enumerable.ToListAsync()这种魔法。如果旧代码只接受
IEnumerable<t></t>,你又必须提供异步数据源,常见做法是: 预加载全部:用
await source.ToListAsync()(适用于小数据集,但失去流式优势) 包装成同步假流(不推荐):用
Task.Run(() => ...).Result强制同步等待(会阻塞线程池线程,可能引发死锁) 重构调用方:让上游也支持
IAsyncEnumerable<t></t>和
await foreach(最干净)
真正需要注意的是:async stream 的状态机更复杂,调试时堆栈更深;而且每个
yield return都是一次状态机跃迁,高频小数据 yield(比如每毫秒 yield 一个 int)反而比同步
yield return开销更大——不是所有“能用 async stream”的地方都“应该用”。
