PAYATHON 2026

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 不代表可以从多个任务同时调用它,也不保证底层操作不会挂起。

备注:内容仅供参考。