Swift 6 严格并发检查实战:从警告到零错误的完整迁移指南
本文是 Swift Concurrency 系列第五章,聚焦 Swift 6 严格并发检查(Strict Concurrency Checking)——从语言演进背景、核心机制、常见编译错误到实战修复策略,帮你平稳迁出"数据竞争地带"。
一、Swift 版本迭代简史
在深入 Swift 6 之前,先回顾一下 Swift 的演进脉络,了解"严格并发"为何成为这一版本的最大主题。
| 版本 | 年份 | 核心主题 |
|---|---|---|
| Swift 1.0 | 2014 | 发布,面向对象 + 函数式,ARC 内存管理 |
| Swift 2.0 | 2015 | 错误处理(do/try/catch),Protocol Extensions,开源 |
| Swift 3.0 | 2016 | 命名规范大重构(API Guidelines),Linux 支持 |
| Swift 4.0 | 2017 | Codable,String 改进,键路径(Key Path) |
| Swift 4.2 | 2018 | CaseIterable,Random API 统一 |
| Swift 5.0 | 2019 | ABI 稳定!Result 类型,字符串插值重构 |
| Swift 5.1 | 2019 | SwiftUI 发布,@propertyWrapper,Opaque Return Types |
| Swift 5.5 | 2021 | async/await,Actor,Sendable(并发奠基版) |
| Swift 5.7 | 2022 | if let 简写,Clock/Duration,Regex Literal |
| Swift 5.9 | 2023 | if/switch 表达式,Macros(宏系统),Parameter Packs |
| Swift 5.10 | 2024 | 完整并发检查预览,全局变量隔离警告 |
| Swift 6.0 | 2024 | 严格并发检查强制开启,数据竞争编译期保证 |
为什么 Swift 6 是里程碑?
Swift 5.5 引入了 async/await 和 Actor,但并发检查默认是宽松的——大量数据竞争隐患不会报错。Swift 6 将其升级为编译错误,Apple 的目标是:
"Swift 6 语言模式使并发安全成为编译期保证,而不仅仅是最佳实践。"
二、什么是严格并发检查?
2.1 三种检查模式
Swift 提供三个并发检查级别(在 Xcode Build Settings 或 Package.swift 中配置):
SWIFT_STRICT_CONCURRENCY = minimal // 默认(Swift 5 行为)
SWIFT_STRICT_CONCURRENCY = targeted // 中间档:对已标注的代码检查
SWIFT_STRICT_CONCURRENCY = complete // Swift 6 行为(最严格)
在 Package.swift 中:
.target(
name: "MyApp",
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency") // Swift 5.9+
// 或直接设置语言版本
]
)
开启 Swift 6 模式(Xcode 16+):
// Swift Language Version = Swift 6
// 或 Package.swift
.target(
swiftLanguageVersions: [.version("6")]
)
2.2 核心保证:消灭数据竞争
严格并发检查的本质是禁止跨并发域的可变状态共享。编译器会强制检查:
- Sendable 合规性:跨 actor/任务边界传递的类型必须是
Sendable - Actor 隔离:访问 actor 的属性/方法必须在 actor 上下文中
- 全局状态隔离:全局/静态变量必须是不可变或被 actor 隔离
- 闭包捕获:
@Sendable闭包中不能捕获非 Sendable 类型
三、常见编译错误与修复实战
3.1 错误一:非 Sendable 类型跨越并发边界
问题代码:
class UserSession { // ❌ 未标注 Sendable
var token: String = ""
var userId: Int = 0
}
let session = UserSession()
Task {
// ❌ Swift 6 Error: Sending 'session' risks causing data races
print(session.token)
}
修复方案 A:改为 struct(值语义,自动 Sendable)
struct UserSession: Sendable { // ✅ struct 默认满足 Sendable
var token: String = ""
var userId: Int = 0
}
修复方案 B:class 显式声明 + 加锁(不推荐新代码)
final class UserSession: @unchecked Sendable { // ⚠️ 需自己保证线程安全
private let lock = NSLock()
private var _token: String = ""
var token: String {
get { lock.withLock { _token } }
set { lock.withLock { _token = newValue } }
}
}
修复方案 C(推荐):改为 Actor
actor UserSession { // ✅ Actor 天然 Sendable + 自动隔离
var token: String = ""
var userId: Int = 0
func updateToken(_ newToken: String) {
token = newToken
}
}
// 使用
let session = UserSession()
Task {
await session.updateToken("abc123")
let t = await session.token
print(t)
}
3.2 错误二:全局可变变量
问题代码:
var globalCache: [String: Data] = [:] // ❌ Swift 6 Error: Global variable is not concurrency-safe
修复方案 A:用 @MainActor 隔离
@MainActor
var globalCache: [String: Data] = [:] // ✅ 只在主线程访问
// 访问时需在主线程上下文
Task { @MainActor in
globalCache["key"] = Data()
}
修复方案 B:用 nonisolated(unsafe) 标注(慎用)
nonisolated(unsafe) var globalCache: [String: Data] = [:]
// ⚠️ 告诉编译器"我保证安全,请不要检查"
修复方案 C:封装进 Actor
actor CacheManager {
static let shared = CacheManager()
private var storage: [String: Data] = [:]
func set(_ data: Data, forKey key: String) {
storage[key] = data
}
func get(forKey key: String) -> Data? {
storage[key]
}
}
3.3 错误三:@Sendable 闭包捕获
问题代码:
class ViewModel: ObservableObject {
var items: [String] = []
func loadData() {
Task {
// ❌ Capture of 'self' with non-sendable type 'ViewModel' in a @Sendable closure
self.items = ["a", "b", "c"]
}
}
}
修复方案:@MainActor + ObservableObject
@MainActor
class ViewModel: ObservableObject { // ✅ @MainActor 隔离整个类
@Published var items: [String] = []
func loadData() {
Task {
let result = await fetchRemote() // 可跨线程执行
items = result // ✅ 自动回到 MainActor
}
}
private func fetchRemote() async -> [String] {
try? await Task.sleep(for: .seconds(1))
return ["a", "b", "c"]
}
}
3.4 错误四:Protocol 中的并发要求
问题代码:
protocol DataLoader {
func load() async -> [String]
}
// ❌ 在某些上下文中 conformance 需要 Sendable
class NetworkLoader: DataLoader {
func load() async -> [String] { ... }
}
修复方案:
// 方案 A:Protocol 本身要求 Sendable
protocol DataLoader: Sendable {
func load() async -> [String]
}
// 方案 B:存在类型约束
protocol DataLoader {
func load() async -> [String]
}
// 使用时加约束
func use<T: DataLoader & Sendable>(_ loader: T) async { ... }
3.5 错误五:@preconcurrency 渐进迁移
当你依赖的旧模块还没有迁移到 Swift 6 时,可以用 @preconcurrency 临时抑制警告:
// 导入尚未迁移的模块
@preconcurrency import SomeOldFramework
// 或在 Protocol Conformance 上
class MyView: @preconcurrency UIViewController {
// ...
}
四、nonisolated 关键字详解
nonisolated 用于在 Actor 中声明某个方法不需要 actor 隔离,可以从任意上下文调用:
actor DataStore {
var data: [String] = []
// ❌ 必须 await 调用
func getData() -> [String] { data }
// ✅ nonisolated:无需 await,可同步调用
nonisolated var description: String {
"DataStore instance" // 只能访问不可变 or nonisolated 的属性
}
// nonisolated 的方法不能访问 actor 隔离的属性
nonisolated func staticInfo() -> String {
"Version 1.0" // ✅ 只访问常量/外部数据
}
}
let store = DataStore()
print(store.description) // ✅ 无需 await
print(await store.getData()) // 需要 await
五、@MainActor 传播规则
@MainActor 会沿着调用链传播,理解这点对排错非常重要:
@MainActor
class HomeViewController: UIViewController {
// 所有方法自动在主线程
func updateUI() {
label.text = "Updated" // ✅
}
// 如需后台工作,用 nonisolated 或 Task.detached
nonisolated func heavyWork() async -> String {
// 在非主线程执行
return "result"
}
func loadAndUpdate() {
Task {
let result = await heavyWork() // 后台
updateUI() // 自动回主线程(因为在 @MainActor 上下文)
}
}
}
传播场景总结:
| 场景 | 结论 |
|---|---|
@MainActor class 内部的方法 | 自动继承 @MainActor |
@MainActor 函数内 await 后 | 仍在主线程 |
Task { } 在 @MainActor 上下文创建 | 继承主线程隔离 |
Task.detached { } | 不继承,独立调度 |
nonisolated 方法 | 脱离 actor 隔离 |
六、渐进迁移策略
Apple 官方推荐的迁移路径(不用一次跳到 Swift 6):
第一步:开启 targeted 检查
SWIFT_STRICT_CONCURRENCY = targeted
只检查已有 async/await 和 Actor 标注的代码,低风险。
第二步:修复所有警告
重点关注:
Sendable警告nonisolated相关警告@MainActor遗漏
第三步:升级到 complete 检查
SWIFT_STRICT_CONCURRENCY = complete
这时会暴露更多隐藏问题,逐一解决。
第四步:切换到 Swift 6 语言版本
SWIFT_VERSION = 6
所有并发检查变为编译错误,不再是警告。
七、实战 Checklist
开启 Swift 6 前,用这份清单扫描你的代码:
- 全局/静态 var → 改为
let、nonisolated(unsafe)、或@MainActor - class 跨线程传递 → 加
Sendable、改struct、或改actor - ObservableObject 子类 → 加
@MainActor - Delegate 回调 → 检查是否在主线程,用
@MainActor声明 Delegate protocol - 第三方库 → 用
@preconcurrency import过渡 - 闭包捕获 → 检查
[weak self]+@Sendable约束
八、小结
Swift 6 的严格并发检查不是为了让你"多写几个关键字",而是把数据竞争这类运行时 Bug 提前消灭在编译阶段。迁移的过程可能短暂痛苦,但带来的收益是:
- 🔒 零数据竞争的类型系统保证
- 🚀 更好的并发性能(编译器可以更大胆优化)
- 🧠 代码意图更清晰(谁在哪个线程做什么,一目了然)
迁移建议:不要急于一次切换到 Swift 6 模式,按
minimal → targeted → complete三步走,每步修完警告再推进。