为什么 IAsyncEnumerable 不能用普通 foreach 遍历
因为
IAsyncEnumerable<t></t>是异步拉取数据的序列,它的每个元素可能需要 await 才能拿到,而传统
foreach是同步迭代器协议(依赖
IEnumerator<t>.MoveNext()</t>和
Current),无法挂起等待。直接写
foreach (var x in asyncStream)会编译失败——C# 编译器只允许在
await foreach语句中使用它。
IAsyncEnumerable 的核心是 IAsyncEnumerator
它本质上是一个“可 await 的枚举器”,关键成员是:
Current:只读属性,返回当前元素(不触发计算)
MoveNextAsync():返回
ValueTask<bool></bool>,真正触发下一项获取(可能 IO、延迟、网络请求等)
每次
await foreach迭代时,编译器会自动展开为循环调用
MoveNextAsync(),并 await 它;成功后才读取
Current。这意味着每一步都可以被调度器中断、切换上下文,且支持取消(通过
CancellationToken参数重载)。
手动实现 IAsyncEnumerable 的两种常见方式
最常用的是用
yield return+
async方法(C# 8+):
public static async IAsyncEnumerable<int> CountDown(int from)
{
for (int i = from; i >= 0; i--)
{
await Task.Delay(100); // 模拟异步工作
yield return i;
}
}编译器会将这个方法转换为一个状态机类,实现
IAsyncEnumerable<t></t>和内部
IAsyncEnumerator<t></t>,自动处理暂停/恢复逻辑。
另一种是手动 new 一个实现类(适合需精细控制生命周期或复用枚举器的场景),但必须注意:
GetAsyncEnumerator()每次调用应返回**新实例**(否则并发
await foreach会冲突);且要正确传播
CancellationToken,否则取消信号会被忽略。
容易被忽略的陷阱:DisposeAsync 和取消传播
IAsyncEnumerable<t></t>的枚举器实现了
IAsyncDisposable,意味着你可以在迭代中途用
await using确保资源释放:
await using var e = numbers.GetAsyncEnumerator();
while (await e.MoveNextAsync())
{
Console.WriteLine(e.Current);
} // DisposeAsync() 自动调用但如果你自己实现
IAsyncEnumerator<t></t>,必须显式支持取消:
MoveNextAsync(CancellationToken)必须传入 token 并参与 await(如
await task.WithCancellation(token))
DisposeAsync()应取消未完成的异步操作,并等待清理完成 不要在
Current中做异步计算——它不该有副作用,也不该 await
否则,用户调用
await foreach时传入
cancellationToken,实际不会中断正在执行的
MoveNextAsync,导致取消失效。
