Swift 中如何在 actor 隔离函数内直接调用异步迭代器?
在题目中的 @MainActor 函数里,调用一次 next(isolation: #isolation) 就可以读取一个元素,并将当前函数的 actor 隔离传给迭代器。SE-0421 规定了这个重载及其调用方式。
直接读取一个元素
以下写法要求工具链和部署目标支持 next(isolation:) 与 #isolation:
@MainActor
func f(seq: some AsyncSequence<Int, any Error>) async throws {
var iter = seq.makeAsyncIterator()
let value = try await iter.next(isolation: #isolation)
print(value as Any)
}
这里调用了一次 next,返回值是 Int?:有元素时返回该元素,序列结束时返回 nil。如果读取过程中发生错误,则通过 throws 向调用方传播。
#isolation 表示调用位置的 actor 隔离,在这个函数里对应 MainActor。配合 isolated 参数,调用方可以把自己的隔离传给被调用函数。SE-0420 说明了这套机制。
为什么 for await 可以编译
在具备相应 API 可用性的上下文中,编译器会将异步循环展开为对 next(isolation: #isolation) 的调用;手写的 iter.next() 则选择无参数重载,两种写法调用的接口不同。SE-0421 的隔离参数说明 规定了这一行为。
题目中的循环可以近似理解为:
var iter = seq.makeAsyncIterator()
while let x = try await iter.next(isolation: #isolation) {
print(x)
break
}
这里展示了与问题相关的展开逻辑,并非完整的编译器生成代码。
为什么无参数 next() 会报错
在题目触发诊断的隔离规则下,无参数 next() 是跨隔离调用。它需要通过可变的 self 访问迭代器,而泛型约束没有保证迭代器是 Sendable,因此编译器会报告潜在的数据竞争。SE-0420 的动机部分 给出了同类示例。
元素类型 Int 是 Sendable,但这不能单独解决问题,因为诊断涉及的是被传递的迭代器本身。
first(where:) 为什么没有解决问题
按题目所述环境,first(where:) 本身是 nonisolated 异步方法。把谓词写成 { _ in true } 只改变筛选条件,不会改变方法的隔离声明,因此传递 seq 时仍可能触发诊断。
如果只需要一个元素,直接使用带隔离参数的 next,也更容易看清调用的隔离关系。
使用时注意什么
next(isolation:)来自 Swift 6 的 SE-0421,实际使用时仍须满足 SDK 和部署目标的可用性要求。如果提示 API 不可用,需要检查具体工具链及目标平台。- 原来的
for try await加break也能表达读取首个元素。空序列不会进入循环体,而直接打印next的结果可能输出nil,两者的输出行为略有区别。 - 应顺序访问同一个迭代器。传入
#isolation不代表可以从多个任务同时调用它,也不保证底层操作不会挂起。
备注:内容仅供参考。
