IAsyncEnumerable 不能直接 new,因其是只读接口且无公开构造函数,必须由编译器从 async + yield return 方法自动生成状态机;手动实现易出错且丢失取消、异常、清理保障。

为什么 IAsyncEnumerable<t></t> 不能直接 new
因为 IAsyncEnumerable<t></t> 是只读接口,没有公开构造函数,也不能手动实现其内部状态机。你无法通过 new 或继承来“造一个”——它必须由编译器从 async enumerable 方法(即返回 IAsyncEnumerable<t></t> 的 async 方法)自动编译生成状态机。试图手写 GetAsyncEnumerator() 实现极易出错,且丢失编译器对取消、异常传播、资源清理的保障。
用 yield return + async 写最简自定义异步迭代器
这是官方推荐、最安全、也最常用的方式:在返回 IAsyncEnumerable<t></t> 的方法中,用 await 和 yield return 混合编写。编译器会将其转换为基于 AsyncIteratorStateMachine 的高效实现。
示例:从 API 分页拉取日志,每页异步等待:
public async IAsyncEnumerable<LogEntry> ReadLogsAsync([EnumeratorCancellation] CancellationToken ct = default)
{
int page = 0;
while (true)
{
var pageData = await _httpClient.GetFromJsonAsync<LogPage>($"/api/logs?page={page}", ct);
if (!pageData.Items.Any()) break;
<pre class="brush:php;toolbar:false;"> foreach (var item in pageData.Items)
{
yield return item; // 同步产出
}
page++;
await Task.Delay(100, ct); // 模拟节流
}}
-
[EnumeratorCancellation]必须加在CancellationToken参数上,否则调用方用WithCancellation(ct)时取消信号不会传入迭代器体 - 所有
await表达式都受该CancellationToken约束(前提是底层 API 正确支持) -
yield return是同步的,不引入额外 await;真正异步发生在await表达式处 - 方法体中抛出的异常会原样传递给消费者(如
await foreach的catch)
什么时候不该用 yield return?改用手动实现 IAsyncEnumerator<t></t>
极少数场景下需要精细控制生命周期,比如:复用已有异步枚举器实例、与非托管资源绑定、或需在 DisposeAsync() 中执行特定清理逻辑。这时可手动实现 IAsyncEnumerator<t></t>,再包装成 IAsyncEnumerable<t></t>。
关键点:
- 必须实现
IAsyncDisposable,并在DisposeAsync()中释放资源(如关闭网络流、取消后台任务) -
MoveNextAsync()返回ValueTask<bool></bool>,避免无谓分配;若内部逻辑本身是同步完成的,应直接返回ValueTask.FromResult(true/false) - 不要在
Current属性里做异步操作——它必须是同步获取 - 手动实现不自动支持
[EnumeratorCancellation],需自行把CancellationToken传入构造并参与每个await
这类实现容易漏掉 DisposeAsync 调用链或状态竞争,除非有明确性能/控制需求,否则优先走 yield return 路线。
await foreach 消费时的隐含行为和坑
消费者看似简单,但有几个关键细节常被忽略:
- 每次循环迭代前,
await foreach会检查CancellationToken是否已触发(如果用了WithCancellation(ct)),但**不会主动中断当前正在执行的MoveNextAsync()** —— 中断是否生效,完全取决于你迭代器内部的await是否响应了该 token - 如果迭代器中
await的操作不接受CancellationToken(如某些老 SDK 的 API),那么WithCancellation就形同虚设 -
await foreach隐式调用DisposeAsync(),但如果循环提前break或发生异常,仍会确保调用;不过若消费者忘了用await foreach而是直接调用GetAsyncEnumerator(),就可能泄漏资源 - 不要在
await foreach循环体内修改正在迭代的集合(即使它是异步流)——这不是线程安全问题,而是语义错误:流本身不可变,但你的消费逻辑若触发了流的重新生成或重置,会导致行为不可预测
异步流的复杂性不在语法,而在取消传播路径、资源生命周期和异常边界的对齐;写的时候多看一眼 CancellationToken 是否真正贯穿到底,比优化 yield 逻辑更重要。









