深入理解 Swift SE-0388:AsyncStream.makeStream 便捷工厂方法的设计与实战
2026/9/23 1:44:21 网站建设 项目流程
  • 文档

【免费下载链接】swift-evolution

This maintains proposals for changes and user-visible enhancements to the Swift Programming Language.

项目地址:https://gitcode.com/gh_mirrors/sw/swift-evolution
点击查看免费下载

导读

SE-0388(Convenience Async[Throwing]Stream.makeStream methods)为 Swift 标准库的AsyncStreamAsyncThrowingStream新增了静态工厂方法makeStream(of:bufferingPolicy:),让开发者可以一步同时获得「流」与其「续体(Continuation)」,彻底告别从初始化闭包中逃逸续体的隐式解包可选(IUO)样板代码。本文以该提案为主体,结合仓库中 SE-0314 AsyncStream 原始提案 与 SE-0406 背压演进提案 的源码级依据,讲解其动机、API 设计、实现细节、缓冲策略参数与兼容性约束,读完后你可以在 Swift 5.9+ 中直接写出更安全、更简洁的生产者/消费者并发代码。

一、背景:AsyncStream 与 Continuation 的角色划分

AsyncStreamAsyncThrowingStream由 SE-0314 引入(Swift 5.5 实现),是标准库提供的「根级 AsyncSequence」类型。它们的作用是把基于回调(callback)或委托(delegate)的多次异步产出,桥接进async/await世界:例如QuakeMonitor.quakeHandler这种每次地震事件回调一次、可被持续调用的接口,就可以包装成AsyncStream<Quake>for await消费。

SE-0314 明确设计了两个互补的角色:

  • 外层AsyncStream<Element>:消费端,对外呈现AsyncSequence接口,通过for await或迭代器的next()逐个取回元素;
  • 内层AsyncStream.Continuation:生产端,Sendable类型,可从任意并发上下文调用yield(_:)产出值、调用finish()结束序列,且可被 yield 多次

两个角色之间的桥梁,就是初始化器接收的那个build闭包——续体通过闭包参数被"交"给生产者。SE-0314 的原始 API 签名如下(节选自 proposals/0314-async-stream.md):

public init( _ elementType: Element.Type = Element.self, bufferingPolicy limit: Continuation.BufferingPolicy = .unbounded, _ build: (Continuation) -> Void )

二、痛点:续体必须从闭包中"逃逸"出来

在实际使用中,一个常见的场景是把续体和流分别交给不同的位置:续体交给生产者(如网络层、事件源),流交给消费者(如 UI 层)。这要求把闭包参数里的Continuation逃逸(escape)出初始化闭包,而 SE-0388 提案指出这种写法存在三个层面的不便:

  1. 必须借助隐式解包可选(IUO):由于闭包在初始化完成后才被调用,逃逸前只能用var cont: AsyncStream<Int>.Continuation!占位;
  2. 触发送达性(Sendability)警告:逃逸后需要再拷贝到let常量里才能消除警告;
  3. 语义误导:闭包结构暗示「续体的生命周期被限定在闭包作用域内」,而实际并非如此——续体需要长期存活,直到流被 finish 或取消。

提案给出了改造前的典型写法(完整示例见 proposals/0388-async-stream-factory.md):

var cont: AsyncStream<Int>.Continuation! let stream = AsyncStream<Int> { cont = $0 } // We have to assign the continuation to a let to avoid sendability warnings let continuation = cont await withTaskGroup(of: Void.self) { group in group.addTask { for i in 0...9 { continuation.yield(i) } continuation.finish() } group.addTask { for await i in stream { print(i) } } }

这段代码虽然能运行,但cont!这种"先占位、后填充"的舞步极易出错,也让"续体生命周期与闭包无关"这一事实变得模糊。

三、方案:新增makeStream静态工厂方法

SE-0388 的解决方案是在AsyncStreamAsyncThrowingStream上各新增一个静态方法makeStream,一次性返回「流 + 续体」二元组。同样是上面的任务组示例,新写法变成了:

let (stream, continuation) = AsyncStream.makeStream(of: Int.self) await withTaskGroup(of: Void.self) { group in group.addTask { for i in 0...9 { continuation.yield(i) } continuation.finish() } group.addTask { for await i in stream { print(i) } } }

对比之下,makeStream方案消除了 IUO、消除了 Sendability 警告、也让"生产端与消费端各持一端"的意图一目了然。提案状态为Implemented (Swift 5.9),即从 Swift 5.9 起可用(仓库 README.md 的发布记录同样确认 Swift 5.9 于 2023-09 发布)。

四、详细设计:完整的 API 签名与实现

4.1 AsyncStream 版本

@available(SwiftStdlib 5.1, *) extension AsyncStream { /// Initializes a new ``AsyncStream`` and an ``AsyncStream/Continuation``. /// /// - Parameters: /// - elementType: The element type of the stream. /// - limit: The buffering policy that the stream should use. /// - Returns: A tuple containing the stream and its continuation. The continuation should be passed to the /// producer while the stream should be passed to the consumer. @backDeployed(before: SwiftStdlib 5.9) public static func makeStream( of elementType: Element.Type = Element.self, bufferingPolicy limit: Continuation.BufferingPolicy = .unbounded ) -> (stream: AsyncStream<Element>, continuation: AsyncStream<Element>.Continuation) { var continuation: AsyncStream<Element>.Continuation! let stream = AsyncStream<Element>(bufferingPolicy: limit) { continuation = $0 } return (stream: stream, continuation: continuation!) } }

4.2 AsyncThrowingStream 版本

@available(SwiftStdlib 5.1, *) extension AsyncThrowingStream { /// Initializes a new ``AsyncThrowingStream`` and an ``AsyncThrowingStream/Continuation``. /// /// - Parameters: /// - elementType: The element type of the stream. /// - failureType: The failure type of the stream. /// - limit: The buffering policy that the stream should use. /// - Returns: A tuple containing the stream and its continuation. The continuation should be passed to the /// producer while the stream should be passed to the consumer. @backDeployed(before: SwiftStdlib 5.9) public static func makeStream( of elementType: Element.Type = Element.self, throwing failureType: Failure.Type = Failure.self, bufferingPolicy limit: Continuation.BufferingPolicy = .unbounded ) -> (stream: AsyncThrowingStream<Element, Failure>, continuation: AsyncThrowingStream<Element, Failure>.Continuation) where Failure == Error { var continuation: AsyncThrowingStream<Element, Failure>.Continuation! let stream = AsyncThrowingStream<Element, Failure>(bufferingPolicy: limit) { continuation = $0 } return (stream: stream, continuation: continuation!) } }

4.3 设计要点逐项解读

  • 参数of elementType:元素类型,默认值Element.self,因此最常见的AsyncStream.makeStream(of: Int.self)与全默认调用AsyncStream.makeStream()都合法;借助类型推断,let (stream, cont) = AsyncStream<Int>.makeStream()也可以省略of:参数。
  • 参数throwing failureType(仅 Throwing 版本):失败类型,约束where Failure == Error,这与 SE-0314 中AsyncThrowingStream.init只允许Failure == Error的构造约束保持一致(见 proposals/0314-async-stream.md)。
  • 参数bufferingPolicy limit:缓冲策略,默认.unbounded,与 SE-0314 初始化器的默认行为一致。其合法取值定义在 SE-0314 的Continuation.BufferingPolicy中(见 proposals/0314-async-stream.md):
取值行为
.unbounded无界缓冲,所有未被消费的 yield 值都先入缓冲区,默认值
.bufferingOldest(Int)缓冲区满时丢弃新到达的元素,保证保留最旧的 n 个值
.bufferingNewest(Int)缓冲区满时丢弃最旧的元素,保证保留最新的 n 个值

需要指出,AsyncStream的缓冲区只为"尚未被迭代消费"的值服务;如果缓冲容量为 0,则当没有任务正在await迭代器的next()时,yield 的值会直接被丢弃(SE-0314 的 dropping 行为)。yield的返回值YieldResultenqueued(remaining:)/dropped(Element)/terminated)正是对这三种情况的显式回报。

  • 返回值是带标签的元组(stream:..., continuation:...):这正是评审后从"具体类型"改为"元组"的关键设计(详见下文备选方案分析)。
  • 实现技巧:方法内部依然使用 IUO——var continuation: ...!配合初始化闭包{ continuation = $0 }完成填充,再用continuation!解包返回。也就是说,SE-0388 不是"消灭"了 IUO,而是把 IUO 从用户代码收拢进标准库内部,用户侧不再接触任何强制解包。
  • 注解组合:方法整体以@available(SwiftStdlib 5.1, *)标记(沿用 SE-0314 类型自身的可用性),同时以@backDeployed(before: SwiftStdlib 5.9)标记,使得该 API 可以向后部署到旧版本 Swift 标准库上运行(详见第七节)。

五、实战:生产者/消费者模式的三种典型写法

5.1 无抛错流(AsyncStream

let (stream, continuation) = AsyncStream.makeStream(of: Int.self) await withTaskGroup(of: Void.self) { group in group.addTask { for i in 0...9 { continuation.yield(i) } continuation.finish() } group.addTask { for await i in stream { print(i) } } }

5.2 可抛错流(AsyncThrowingStream

SE-0314 曾给出过一个把回调式"买菜"接口桥接为流的例子(见 proposals/0314-async-stream.md),用makeStream改写后,生产端可以直接把续体交给回调接口,无需闭包嵌套:

let (stream, continuation) = AsyncThrowingStream.makeStream(of: Vegetable.self) buyVegetables( shoppingList: list, onGotVegetable: { veggie in continuation.yield(veggie) }, onAllVegetablesFound: { continuation.finish() }, onNonVegetable: { error in continuation.finish(throwing: error) } ) for try await veggie in stream { // 消费蔬菜 }

注意AsyncThrowingStream的消费需要使用for try await,因为迭代器next()可能抛出错误;finish(throwing:)传入的错误会被迭代器原样抛出,而finish()则表示正常结束。

5.3 处理任务取消与资源清理

makeStream返回的续体同样支持onTermination回调。SE-0314 规定(见 proposals/0314-async-stream.md):当迭代结束、流离开作用域或所在任务被取消时,会触发onTermination,其中Termination区分.finished.cancelled两种终态。配合makeStream可写成:

let (stream, continuation) = AsyncStream.makeStream(of: Quake.self) continuation.onTermination = { termination in switch termination { case .finished: monitor.stopMonitoring() case .cancelled: monitor.stopMonitoring() } } monitor.quakeHandler = { quake in continuation.yield(quake) } monitor.startMonitoring() for await quake in stream { // 处理地震事件 }

六、从源码结构看:为什么返回元组而不是具体类型

提案在 Alternatives considered 一节完整记录了设计权衡,这一决策过程对理解 API 形态至关重要。

6.1 元组 vs 具体类型

提案者最初的 pitch 使用元组作为工厂的返回类型,评审前又改成了具体类型(理由是便于写文档注释);但在正式评审中,多数反馈倾向于元组方案,最终接受时改回元组(见提案末尾 Revision history:"After review: Changed the return type from a concrete type to a tuple")。元组方案的两个主要收益:

  1. 引导解构(destructuring)let (stream, continuation) = ...的写法自然地把两个值拆开,分别交给生产者与消费者各自持有——这正是期望的使用方式;
  2. 支持向后部署(back deployment):具体类型方案无法同样轻量地实现@backDeployed

6.2 为什么不在init中直接传入续体

pitch 阶段有人提议让用户直接把一个续体传给AsyncStream<Element>.init(),该方案被否决,原因有二:

  1. 同一个续体可能被传入多个流,造成一对多、语义混乱;
  2. 未被任何流持有的续体毫无用处。

SE-0388 的结论是:AsyncStream.Continuation与某个AsyncStream实例深度耦合(一一对应),因此 API 应当把这种耦合关系直接表达出来,从类型层面杜绝误用。

6.3 为什么不"什么都不做"

提案也考虑了维持现状的选项,但认为既然AsyncStream属于标准库,就应该提供一个更体面的方式来同时创建流与续体,而非要求用户手写 IUO 逃逸样板。

七、兼容性与演进:source compatibility、ABI 与 API resilience

SE-0388 对兼容性的影响全部是纯增量的:

  • Source compatibility:新增静态方法,不修改任何既有声明,对源码完全无影响;
  • Effect on ABI stability:仅引入新的并发库 ABI(makeStream方法本身),不影响既有声明的 ABI;
  • Effect on API resilience:无影响,既有弹性模型允许添加静态方法;
  • 向后部署@backDeployed(before: SwiftStdlib 5.9)意味着在 Swift 5.9 之前的标准库运行时上,编译器会在调用点内联该方法的实现,使旧系统也能获得这一便捷 API。

八、后续演进:SE-0406 中带背压的makeStream变体

makeStream的工厂形态并非终点。仓库中的 SE-0406 AsyncStream backpressure 在其上进一步演进:引入makeStream(of:backpressureStrategy:),返回(stream, source)source提供write(contentsOf:)enqueueCallback等带背压的写入 API,支持.watermark(low:high:)等策略(见 proposals/0406-async-stream-backpressure.md):

let (stream, source) = AsyncStream.makeStream( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 4) )

SE-0406 还明确将其定位为严格单播(strict unicast)异步序列,并讨论了makeStream方法中提供onTerminate回调的方案。这说明「一次性返回流 + 生产端」这一工厂模式已成为 Swift 异步流 API 的标准骨架,后续演进均在此基础上叠加能力,而 SE-0388 正是这一模式的奠基者。

九、总结

SE-0388 的makeStream以极小的 API 面(两个静态方法)解决了AsyncStream使用中最常见的痛点:续体逃逸样板。它把隐式解包可选收敛进标准库实现、用元组返回引导正确的解构语义、并以@backDeployed实现跨版本可用。在 Swift 5.9 及以后,let (stream, continuation) = AsyncStream.makeStream(of: Element.self)应成为所有生产者/消费者桥接代码的默认起点;若你需要更精细的背压控制,则可进一步关注 SE-0406 的演进方案。相关完整资料可继续阅读仓库内的 SE-0388 原文、SE-0314 AsyncStream 设计 与 SE-0406 背压演进。

  • 文档

【免费下载链接】swift-evolution

This maintains proposals for changes and user-visible enhancements to the Swift Programming Language.

项目地址:https://gitcode.com/gh_mirrors/sw/swift-evolution
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询