Highlight
Swift 6 语言模式通过编译期强制执行数据隔离,阻止跨 actor 共享可变状态导致的数据竞争。
核心内容
你写了一个 App,用了 @MainActor、async/await、Sendable,看起来已经”安全”了。但编译器其实没有阻止你把一个 class 实例从 main actor 传给另一个 actor——两个 actor 同时持有同一个引用类型,就可能出现数据竞争,轻则崩溃,重则损坏用户数据。
Swift 6 语言模式解决的就是这个问题。它引入了完整的数据隔离强制执行:编译器会在编译期阻止所有不安全的跨 actor 共享状态。你新增功能或重构代码时,编译器替你把关,不会让新的并发 bug 悄悄溜进来。
Session 以 CoffeeTracker App 为例演示了完整迁移流程。这个 App 之前已经迁移到了 Swift concurrency,但开启 Swift 6 的 Complete Concurrency Checking 后仍然冒出十几个警告。迁移策略是按 target 逐步推进:先对每个 target 开启 Complete Concurrency Checking(产生警告但不阻止编译),逐条解决后再开启 Swift 6 Language Mode(将警告升级为错误,锁定安全状态)。Session 反复强调一个原则:不要同时做大规模重构和开启 Swift 6,应该分开进行。
详细内容
迁移步骤:逐 target 推进
每个 target 的迁移遵循固定流程:(07:35)
- 开启 Complete Concurrency Checking——项目仍处于 Swift 5 模式,但编译器会对所有可能在 Swift 6 下不安全的代码发出警告
- 逐条解决所有警告
- 开启 Swift 6 Language Mode——锁定改动,防止后续代码回退到不安全状态
- 进入下一个 target 重复上述步骤
Ben 建议从 App extension / UI 层开始迁移,而非底层框架。原因有二:UI 层大部分代码本身就在 main thread 上运行,警告较少;且上层迁移后,与未迁移框架的交互问题会暴露出来,可以用 @preconcurrency 等工具处理。
全局变量:var 改 let 是最常见的快速修复
开启 Complete Checking 后最常见的警告是全局 var。全局变量是共享可变状态,任何线程都能读写,天然存在数据竞争风险。编译器提供三种修复方式:(13:38)
// 选项 1:var 改 let——Logger 是 Sendable 类型,let 声明后不可变,多线程安全
let logger = Logger(
subsystem:
"com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView",
category: "Root View")
// 选项 2:用 @MainActor 隔离——所有访问必须在 main actor 上
@MainActor var logger = Logger(
subsystem:
"com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView",
category: "Root View")
// 选项 3:nonisolated(unsafe)——绕过检查,由你自己保证安全
nonisolated(unsafe) var logger = Logger(
subsystem:
"com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView",
category: "Root View")
关键点:
let是首选方案——Sendable 类型一旦声明为let,就不存在共享可变状态的问题@MainActor适合确实需要可变但只从 main actor 访问的全局变量nonisolated(unsafe)是最后手段,它把安全责任交给了你自己,编译器不再检查;后续应回过头用正确架构替代它
Ben 特别提到,Swift 的全局变量是延迟初始化且原子创建的(16:29),不存在两个线程同时首次访问导致创建两个实例的问题。
跨 actor 发送数据:让类型遵循 Sendable
当 main actor 上的 CoffeeData 向另一个 actor 上的 CoffeeDataStore 发送 [Drink] 数组时,编译器发出警告:发送 self.currentDrinks 可能导致数据竞争(30:38)。解决方式是让 Drink 遵循 Sendable:(33:29)
public struct Drink: Hashable, Codable, Sendable {
public let mgCaffeine: Double
public let date: Date
public let uuid: UUID
public let type: DrinkType?
// ...
}
关键点:
Drink是 struct,所有属性都是let的值类型,天然符合 Sendable 语义internal类型 Swift 会自动推断 Sendable,但public类型必须显式声明——因为 Sendable 是对调用方的承诺,你可能未来需要内部可变状态,不想提前锁定- 给
Drink加Sendable一行改动消除了三处警告——寻找这类”一处改动消灭大量警告”的根因是高效迁移的关键
加上 Sendable 后,DrinkType 也需要同步标记为 Sendable:(35:04)
public enum DrinkType: Int, CaseIterable, Identifiable, Codable, Sendable {
// ...
}
如果 DrinkType 是一个无法遵循 Sendable 的 Objective-C 类型,可以用 nonisolated(unsafe) 标注该属性作为过渡方案,但这意味着你承担了安全责任。
处理未迁移框架的 protocol 隔离问题
当你的 @MainActor 类型需要遵循一个未标记 actor 隔离的 protocol 时,编译器会报错:Main actor-isolated 方法无法满足 nonisolated 的 protocol requirement(9:38)。Session 展示了两种解决方式:
方式一——@preconcurrency 标注 conformance:(25:21)
extension Recaffeinater: @preconcurrency CaffeineThresholdDelegate {
public func caffeineLevel(at level: Double) {
if level < minimumCaffeine {
// TODO: alert user to drink more coffee!
}
}
}
关键点:
@preconcurrency等价于用MainActor.assumeIsolated包裹方法体——它告诉编译器”我知道这个回调会在当前 actor 上执行”- 如果运行时回调实际不在 main actor 上,程序会 trap(而非产生难以追踪的数据竞争)
- 当底层框架也迁移到 Swift 6 并给 protocol 加了
@MainActor后,@preconcurrency会产生”不再需要”的警告,直接删除即可(26:50)
方式二——手动用 nonisolated + MainActor.assumeIsolated:(24:15)
nonisolated public func caffeineLevel(at level: Double) {
MainActor.assumeIsolated {
if level < minimumCaffeine {
// TODO: alert user to drink more coffee!
}
}
}
这等价于 @preconcurrency 的手动展开版,适合需要更细粒度控制的场景。
CoreLocation delegate 回调的隔离处理
CoreLocation 的 CLLocationManagerDelegate 回调线程取决于创建 CLLocationManager 的线程——这是一个编译期无法静态检查的动态属性。Session 的做法是把整个 delegate 类放到 @MainActor 上,确保 manager 在 main thread 创建,回调也就在 main thread:(39:32)
@MainActor
class CoffeeLocationDelegate: NSObject, CLLocationManagerDelegate {
var location: CLLocation?
var manager: CLLocationManager!
var latitude: CLLocationDegrees? { location?.coordinate.latitude }
var longitude: CLLocationDegrees? { location?.coordinate.longitude }
override init () {
super.init()
manager = CLLocationManager()
manager.delegate = self
manager.startUpdatingLocation()
}
nonisolated func locationManager (
_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]
) {
MainActor.assumeIsolated { self.location = locations.last }
}
}
关键点:
- 类标注
@MainActor后,CLLocationManager在 main thread 上创建,CoreLocation 据此将回调也派发到 main thread locationManager(_:didUpdateLocations:)标记为nonisolated,因为 protocol 要求没有 actor 隔离- 内部用
MainActor.assumeIsolated访问 main actor 上的属性,如果运行时不在 main actor 则 trap
核心启发
-
做什么:先开启 Complete Concurrency Checking 再逐条修警告,而非手动审计 为什么值得做:编译器会精确指出哪些代码不安全,比人工逐行排查效率高得多。开启后项目仍能正常编译运行,不阻塞交付。 怎么开始:在 Xcode 中选中 target -> Build Settings -> 搜索 “Swift Concurrency Checking” -> 设为 Complete。
-
做什么:优先把全局
var改成let为什么值得做:这是迁移中数量最多、修复最简单的警告类型。Session 中 CoffeeKit 的 11 个警告里,7 个是全局var,一行let替换即可消除。 怎么开始:开启 Complete Checking 后,按警告列表逐一检查,把不会被重新赋值的全局var改为let。 -
做什么:给跨 actor 传递的 public 值类型加
Sendable为什么值得做:一个Sendable标注往往能消除多处 “Sending … risks causing data races” 警告。Session 中Drink加Sendable一行改了三处警告。 怎么开始:定位 “Sending … risks causing data races” 警告中涉及的类型,如果该类型是 struct/enum 且所有属性都是值类型或 Sendable 类型,直接加Sendable遵循。 -
做什么:对未迁移框架的 protocol conformance 使用
@preconcurrency为什么值得做:当你确认回调实际会在某个 actor 上执行,但 protocol 还没标注隔离时,@preconcurrency是最简洁的声明方式。等底层框架迁移后,编译器会自动提醒你删除它。 怎么开始:遇到 “Main actor-isolated instance method cannot satisfy nonisolated protocol requirement” 警告时,在 conformance 声明处加@preconcurrency。
关联 Session
- A Swift Tour: Explore Swift’s features and design — Swift 语言核心特性与设计哲学概览
- Demystify explicitly built modules — Xcode 16 显式模块构建机制的变化
- Consume noncopyable types in Swift — Swift 非复制类型(noncopyable)的用法与并发安全
- Explore Swift performance — Swift 编译机制与不同代码模式对性能的影响
评论
GitHub Issues · utterances