如果团队开启过Swift 6模式,应该记得那一刻:原本运行正常的项目突然涌出几十、几百个并发错误。
错误信息的主角大多只有一个:Sendable。
Concurrency系列第3篇整理最后这块拼图,以及Swift 6的strict concurrency。
Sendable提出的问题——这个值可以跨越边界吗
在actor篇中,我们将隔离概括为“串行执行上下文”:actor内部、MainActor之上,以及不属于二者的协作池。
但值会频繁跨越这些边界。它们作为actor方法的参数进入,作为返回值离开,也会被Task闭包捕获。
风险由此产生。隔离保护的是actor自身的状态,但如果跨越边界的值是可变引用类型呢?
同一个实例可能同时被两个隔离上下文持有,actor阻止的数据竞争会通过带入的对象重新出现。就像边境检查很严格,却不检查入境物品。
Sendable就是这套检查标准。它是没有要求方法的标记协议,含义只有一个:
这种类型的值跨越隔离边界后仍可安全地并发使用。
哪些类型是安全的?直觉上很简单。
值类型跨越时会被复制,因此是安全的(前提是所有存储属性都是Sendable)。Int、String,以及仅由Sendable属性组成的struct和enum都属于此类。大多数情况下,编译器会自动认可它们。
actor也很安全,因为它内置了自我保护。
不可变类(final且只有let属性)也安全。既然无法修改,就不会有竞争。
真正危险的只有一种:拥有可变状态的类。值类型优先篇中的“共享+可变=危险”,正是Sendable的判定标准。
函数类型有单独的@Sendable标记。Task { }中的闭包就是典型的@Sendable闭包,带有该标记的闭包不能捕获non-Sendable值。
闭包篇提到过,捕获可能成为跨越隔离边界的走私通道,因此语言会检查通道本身。
strict concurrency——将警告变成错误,将规范变成检查
这些规则在Swift 6之前就存在,但默认情况下基本保持沉默。
Swift 6语言模式的核心,就是开启全部检查并将其提升为错误,也就是strict concurrency。在complete checking下,non-Sendable值跨越隔离边界的每个位置都会变成编译错误。
错误大量出现,并不是因为代码突然变差了,而是原本潜在的竞争现在才显现出来。
这就像引入可选值时,所有“可能为nil的地方”都暴露出来。那时需要nil检查,如今则是把并发假设迁移到类型系统中的过渡期。
好在编译器也越来越聪明。Swift 6加入的基于区域的隔离分析(Swift Evolution提案SE-0414,region-based isolation)就是例子。
即使是non-Sendable值,只要能证明“发送方不会再触碰它”,也允许移动。
所有权已经转移的值不可能制造竞争。因此,许多理论上应该报错的代码实际上仍能通过。
sending参数标记也遵循同样的方向。总之,规则正从“一律禁止”变得更精确,转向“证明安全后允许”。
实战迁移——按错误类型处理
大量错误大多会归结为几种模式。下面整理各类型的标准处理方案。
类型1。我的模型是non-Sendable。 这是最常见、也最健康的错误。首选方案是改成struct。
如果不需要引用身份的数据模型被声明成了class,这正是迁移为值类型的理由。
如果必须是class,就用final + let使其不可变并采用Sendable。如果必须可变,说明该状态需要所有者,因此应考虑升级为actor。
类型2。全局变量和static var。 之前警告过“static var本质上是全局状态”的所有位置都会变成错误。
如果确实需要全局状态,就声明隔离。UI相关内容加上@MainActor,否则用actor封装或改为不可变的let。
类型3。委托或回调类卡在边界上。 这经常出现在与UIKit时代API交互的地方。
如果该类型实际上只供主线程使用,声明@MainActor通常就是答案。把“这个类原本就只在主线程使用”的隐含事实改成显式声明。
类型4。实际上安全,但编译器不知道。 例如内部由锁保护的类,以及C库包装器。
此时的出口是@unchecked Sendable。它声明“安全性由我保证,请关闭检查”,但名称中的unchecked已经提醒这是unsafe系列工具。
遵循哲学篇第1篇中的显式出口原则,在注释中留下保证依据(哪把锁保护什么),并将其限制在最小范围内。
如果为了迁移方便开始用unchecked覆盖错误,最后得到的只是关闭检查、却挂着Swift 6标志的代码。
策略上不必一次性全部开启。语言模式可以按模块选择。
标准做法是自下而上:先将依赖较少的末端模块(工具和模型)升级到Swift 6模式,最后再升级应用target。
也可以使用Xcode的upcoming feature标志,提前只提高检查级别并观察警告。
读懂方向——为什么要让我们经历这些?
迁移的痛苦确实存在,因此有必要准确理解这项成本的依据。
Swift 6的承诺是:能编译就没有数据竞争。无法复现的间歇性崩溃,以及发布后才出现的时序错误,这些问题类别都会在编译时消失。
这与内存安全(可选值、ARC——Automatic Reference Counting,自动引用计数)走过的道路相同。并发安全正从“写得好就行”转变为“由语言保证”。
这一方向也是哲学系列轨迹的延伸:将容易出错的规则提升到类型系统中,在有成本的地方要求显式标记(@unchecked、sending),再通过渐进式采用(模块级语言模式)吸收过渡期摩擦。
Swift Evolution篇中提到的流程仍在通过加入默认隔离选项等缓冲机制来降低摩擦。现在遇到的警告正处于这次转变的中间阶段。
总结
- Sendable标记的是能够在跨越隔离边界后安全并发使用的类型。值类型、actor和不可变类是安全的;可变类就是全部危险来源。
- @Sendable闭包会检查捕获,防止闭包成为竞争的走私通道。
- Swift 6 strict concurrency将这些检查提升为错误。错误爆发并不表示代码变差,而是潜在竞争暴露出来了。
- 处理优先级:值类型化 → 不可变化 → 升级为actor → 声明隔离(@MainActor)→ 最后使用明确记录依据的@unchecked Sendable。
- 从末端模块开始自下而上迁移,语言模式按模块逐步提升。
下一篇是Concurrency系列的收尾:结构化并发。内容包括Task、async let和TaskGroup构成的任务树,以及取消(cancellation)如何沿着这棵树传播。

![[Swift进阶 #3] Sendable与Swift 6并发错误迁移 封面图](/assets/images/posts/053e9ee5-0023-4c58-838f-c7d6d31cf70e/swift-sendable-strict-concurrency-1.jpg)