Swift 6 严格并发报错怎么改?11 个 Sendable、MainActor 并发报错
Swift 6 严格并发把数据竞争检查变成错误,常见报错集中在非 Sendable 值跨隔离域、非隔离上下文访问 @MainActor 状态和全局可变变量。本文列出 11 条报错原文与改法,并讲在 Swift 5 模式下渐进迁移。
Swift 6 严格并发报错的改法基本固定。Swift 6 语言模式把数据竞争检查从警告变成了错误,最常见的报错集中在三类:非 Sendable 的值跨隔离域传递、在非隔离上下文里访问 @MainActor 的状态、全局或静态的可变变量。对应的改法是:界面相关的代码整体放到 @MainActor,跨线程传的数据改成值类型或 Sendable,共享的可变状态交给 actor。下面列出 11 条报错的原文、原因和改法代码,并讲 Swift 5 模式下的渐进迁移。
报错原文取自 Swift 6.4 编译器的实际输出,其他版本措辞略有出入,按报错里的关键词对照即可。有几条因为系统 API 带有 @preconcurrency 标注,在 Swift 6 模式下目前显示为警告,问题是真实的,同样要改。官方完整说明见 swift.org 的 Swift 6 并发迁移指南和 Apple 的 Adopting strict concurrency in Swift 6 apps。
隔离域、Sendable 和跨边界分别是什么意思?
- 隔离域。同一时刻只允许一段代码访问的范围。
@MainActor是主线程这个隔离域,每个actor实例各是一个隔离域,什么都没标的代码是非隔离(nonisolated)的,可以在任何线程运行。 - Sendable。能安全地从一个隔离域交给另一个隔离域的类型。只含 Sendable 成员的非 public 结构体和枚举自动是 Sendable,
actor和标了@MainActor的类也是。普通的class默认不是。 - 跨边界。值从一个隔离域到另一个隔离域,例如传进
actor的方法、在后台闭包里用到主线程的对象。编译器只在能证明安全时放行。
UIViewController、UIView 和 SwiftUI 的 View 本身就是 @MainActor 的,你的子类和 body 里的代码都在主线程隔离域。很多报错就是因为主线程的对象被带进了后台闭包。
1. static var 不是并发安全的全局状态
1static property 'shared' is not concurrency-safe because it is nonisolated global shared mutable state
static var 和全局 var 任何线程都能读写,编译器无法保证没有数据竞争。
1final class AppSettings {
2 static var shared = AppSettings()
3 var apiBase = "https://api.example.com"
4}
单例只在界面里用的,把整个类放到主线程,var 改成 let。
1@MainActor
2final class AppSettings {
3 static let shared = AppSettings()
4 var apiBase = "https://api.example.com"
5}
只是常量的,直接用 let 存 Sendable 的值。
1enum API {
2 static let base = URL(string: "https://api.example.com")!
3}
写成 static let shared = AppSettings() 但类不是 Sendable,会换成另一条报错 static property 'shared' is not concurrency-safe because non-'Sendable' type 'AppSettings' may have shared mutable state,改法相同。确实用锁或队列保护了访问的,可以写 nonisolated(unsafe) static var,编译器不再检查,安全由你自己负责。
2. 在非隔离上下文访问主线程属性怎么改?
1main actor-isolated property 'count' can not be referenced from a nonisolated context
count 属于主线程,badgeText 是没标隔离的普通函数,可能在任何线程被调用。
1@MainActor
2final class CartModel {
3 var count = 0
4}
5
6func badgeText(for cart: CartModel) -> String {
7 "\(cart.count)"
8}
这个函数只给界面用,就给它也标上 @MainActor。
1@MainActor
2func badgeText(for cart: CartModel) -> String {
3 "\(cart.count)"
4}
改成写入时,报错里的 referenced 会变成 mutated,改法一样。
3. 在 async 函数里直接读主线程属性
1main actor-isolated property 'count' cannot be accessed from outside of the actor
较早的编译器报的是 expression is 'async' but is not marked with 'await'。在非隔离的 async 函数里读主线程的属性,要先切到主线程,这一步是异步的,必须写 await。
1func makeRequest(cart: CartModel) async -> URLRequest {
2 var request = URLRequest(url: URL(string: "https://api.example.com/cart")!)
3 request.setValue("\(cart.count)", forHTTPHeaderField: "X-Count")
4 return request
5}
先 await 取出值,再使用。
1func makeRequest(cart: CartModel) async -> URLRequest {
2 let count = await cart.count
3 var request = URLRequest(url: URL(string: "https://api.example.com/cart")!)
4 request.setValue("\(count)", forHTTPHeaderField: "X-Count")
5 return request
6}
4. 回调闭包里调用主线程方法
1call to main actor-isolated instance method 'show' in a synchronous nonisolated context
URLSession 的完成回调是 @Sendable 闭包,在后台线程执行,不能同步调用视图控制器的方法。这一条目前是警告,但后台线程刷新界面的问题是真实的。旧代码里用 DispatchQueue.main.async 包一层的地方,大多属于这一类。
1final class FeedViewController: UIViewController {
2 func show(_ data: Data) {
3 // 刷新界面
4 }
5
6 func fetch(_ url: URL) {
7 URLSession.shared.dataTask(with: url) { data, _, _ in
8 guard let data else { return }
9 self.show(data)
10 }.resume()
11 }
12}
改成 async/await。方法本身在主线程,await 期间网络请求在后台进行,回来后接着在主线程执行。data(from:) 从 iOS 15 开始提供。
1final class FeedViewController: UIViewController {
2 override func viewDidLoad() {
3 super.viewDidLoad()
4 Task {
5 await fetch(URL(string: "https://api.example.com/feed")!)
6 }
7 }
8
9 func show(_ data: Data) {
10 // 刷新界面
11 }
12
13 func fetch(_ url: URL) async {
14 do {
15 let (data, _) = try await URLSession.shared.data(from: url)
16 show(data)
17 } catch {
18 print("请求失败:\(error)")
19 }
20 }
21}
暂时不想改回调写法的,在闭包里写 Task { @MainActor in self.show(data) }。Data 是 Sendable,可以带过去。
5. 协议一致性跨隔离(delegate 最常见)
1conformance of 'PlayerViewController' to protocol 'PlayerDelegate' crosses into main actor-isolated code and can cause data races
Swift 6.0 的写法是 main actor-isolated instance method 'playerDidFinish()' cannot be used to satisfy nonisolated protocol requirement。协议没标隔离,意味着任何线程都可能调用 playerDidFinish(),而实现它的视图控制器在主线程。
1protocol PlayerDelegate: AnyObject {
2 func playerDidFinish()
3}
4
5final class PlayerViewController: UIViewController, PlayerDelegate {
6 private var statusText = ""
7
8 func playerDidFinish() {
9 statusText = "播放完毕"
10 }
11}
按情况选一种。
- 协议是你自己的,回调总在主线程。给协议标
@MainActor,最干净。
1@MainActor
2protocol PlayerDelegate: AnyObject {
3 func playerDidFinish()
4}
- Swift 6.2 起,可以声明只在主线程成立的一致性,协议本身不用改。
1final class PlayerViewController: UIViewController, @MainActor PlayerDelegate {
2 private var statusText = ""
3
4 func playerDidFinish() {
5 statusText = "播放完毕"
6 }
7}
- 系统或第三方协议,文档说明回调在主线程。实现标
nonisolated,用MainActor.assumeIsolated回到主线程。不在主线程调用时这里会直接崩溃,能尽早暴露问题。
1nonisolated func playerDidFinish() {
2 MainActor.assumeIsolated {
3 self.statusText = "播放完毕"
4 }
5}
- 回调可能来自后台线程。实现标
nonisolated,在里面用Task { @MainActor in ... }切回主线程。 - 还没迁移的第三方协议。在一致性前加
@preconcurrency,写成UIViewController, @preconcurrency PlayerDelegate,编译错误变为运行时检查。
6. 把非 Sendable 的值传进 actor 为什么报错?
1sending 'draft' risks causing data races
Draft 是普通 class,交给 actor 之后,你这边和 actor 里同时持有同一个对象。后面那行 print 还在用它,编译器就报错。
1final class Draft {
2 var text = ""
3}
4
5actor DraftStore {
6 private var drafts: [Draft] = []
7
8 func save(_ draft: Draft) {
9 drafts.append(draft)
10 }
11}
12
13func submit(to store: DraftStore) async {
14 let draft = Draft()
15 draft.text = "你好"
16 await store.save(draft)
17 print(draft.text)
18}
数据模型改成结构体。只含 Sendable 成员的非 public 结构体自动是 Sendable,传进去的是一份拷贝。
1struct Draft {
2 var text = ""
3}
暂时不能改成结构体的,传进 actor 之后就不要再使用这个对象,删掉后面的 print,报错也会消失。结构体是 public 的不会自动推断,要显式写 : Sendable。
7. @Sendable 闭包捕获了非 Sendable 的 self
1capture of 'self' with non-Sendable type 'ThumbnailCache' in a '@Sendable' closure
DispatchQueue.global().async 的闭包是 @Sendable 的。这一条目前是警告,但多个后台闭包同时改 images 字典,就是真实的数据竞争。
1final class ThumbnailCache {
2 private var images: [URL: Data] = [:]
3
4 func preload(_ url: URL) {
5 DispatchQueue.global().async {
6 if let data = try? Data(contentsOf: url) {
7 self.images[url] = data
8 }
9 }
10 }
11}
需要在多个线程间共享的缓存,改成 actor,对 images 的访问自动排队。
1actor ThumbnailCache {
2 private var images: [URL: Data] = [:]
3
4 func image(for url: URL) async throws -> Data {
5 if let cached = images[url] {
6 return cached
7 }
8 let (data, _) = try await URLSession.shared.data(from: url)
9 images[url] = data
10 return data
11 }
12}
8. 并发闭包里修改捕获的变量
1mutation of captured var 'finished' in concurrently-executing code
多个后台闭包同时对一个局部变量 += 1,结果不确定。这一条目前也是警告。
1func uploadAll(_ files: [URL]) {
2 var finished = 0
3 for file in files {
4 DispatchQueue.global().async {
5 upload(file)
6 finished += 1
7 }
8 }
9}
改用任务组,子任务只返回结果,计数在汇总时做。
1func uploadAll(_ files: [URL]) async -> Int {
2 await withTaskGroup(of: Bool.self) { group in
3 for file in files {
4 group.addTask { await upload(file) }
5 }
6 var finished = 0
7 for await ok in group where ok {
8 finished += 1
9 }
10 return finished
11 }
12}
13
14func upload(_ file: URL) async -> Bool {
15 // 上传文件,成功返回 true
16 true
17}
9. Task.detached 里怎么更新主线程状态?
1main actor-isolated property 'items' can not be mutated from a nonisolated context
Task.detached 不继承当前的隔离,闭包在后台执行,不能直接写主线程的属性。
1@MainActor
2final class FeedModel {
3 var items: [String] = []
4
5 func reload(_ data: Data) {
6 Task.detached {
7 let parsed = FeedModel.parse(data)
8 self.items = parsed
9 }
10 }
11
12 nonisolated static func parse(_ data: Data) -> [String] {
13 // 耗时的解析
14 []
15 }
16}
让后台任务只负责算出结果,用 .value 等它返回,赋值回到主线程做。
1func reload(_ data: Data) async {
2 let parsed = await Task.detached(priority: .userInitiated) {
3 FeedModel.parse(data)
4 }.value
5 items = parsed
6}
Swift 6.2 起可以把解析函数标成 @concurrent,它一定在后台线程池执行,调用方直接 await 即可,不用再写 Task.detached。
1func reload(_ data: Data) async {
2 items = await FeedModel.parse(data)
3}
4
5@concurrent
6static func parse(_ data: Data) async -> [String] {
7 []
8}
Task { } 和 Task.detached { } 的区别在于前者继承当前隔离。在视图控制器里写 Task { },里面的代码仍然在主线程,耗时计算会卡界面。
10. Sendable 类里有可变属性怎么改?
1stored property 'value' of 'Sendable'-conforming class 'Counter' is mutable
为了消掉别的报错给 class 加上 Sendable,编译器会检查它是不是真的安全。
1final class Counter: Sendable {
2 var value = 0
3}
改成 actor。
1actor Counter {
2 private(set) var value = 0
3
4 func increment() {
5 value += 1
6 }
7}
需要同步调用、最低版本是 iOS 18 的,可以用 Synchronization 框架的 Mutex。
1import Synchronization
2
3@available(iOS 18.0, *)
4final class Counter: Sendable {
5 private let value = Mutex(0)
6
7 func increment() {
8 value.withLock { $0 += 1 }
9 }
10
11 var current: Int {
12 value.withLock { $0 }
13 }
14}
更低版本、已经用 NSLock 保护了所有访问的,可以声明为 @unchecked Sendable,安全由你自己保证。
11. 主线程类型用作非隔离的默认值
1main actor-isolated default value in a nonisolated context
ProfileViewModel 自己写了 init,这个初始化方法属于主线程。AppDependencies 没标隔离,它的属性默认值可能在任何线程计算。
1@MainActor
2final class ProfileViewModel {
3 var name: String
4
5 init(name: String = "") {
6 self.name = name
7 }
8}
9
10final class AppDependencies {
11 let profile = ProfileViewModel()
12}
AppDependencies 只在界面里用,就给它也标 @MainActor。初始化里只给 Sendable 的属性赋值、不碰别的主线程状态的,也可以把 init 标成 nonisolated。
1@MainActor
2final class ProfileViewModel {
3 var name: String
4
5 nonisolated init(name: String = "") {
6 self.name = name
7 }
8}
没写 init、所有属性都有简单默认值的主线程类,编译器生成的初始化方法本身就是 nonisolated,不会报这一条。
Swift 6 严格并发报错怎么快速对照?
| 报错关键词 | 原因 | 首选改法 |
|---|---|---|
is not concurrency-safe because it is nonisolated global shared mutable state |
全局或 static var |
改 let,或标 @MainActor |
non-'Sendable' type ... may have shared mutable state |
static let 存了非 Sendable 对象 |
类型标 @MainActor 或做成 Sendable |
can not be referenced from a nonisolated context |
普通函数访问主线程属性 | 函数标 @MainActor |
cannot be accessed from outside of the actor / expression is 'async' but is not marked with 'await' |
异步函数里读其他隔离域的属性 | 加 await |
call to main actor-isolated ... in a synchronous nonisolated context |
后台回调里调用主线程方法 | 改 async/await,或 Task { @MainActor in } |
crosses into main actor-isolated code / cannot be used to satisfy nonisolated protocol requirement |
delegate 协议没标隔离 | 协议标 @MainActor,或 @MainActor 一致性 |
sending '...' risks causing data races |
非 Sendable 对象传进 actor 后还在用 | 改结构体,或传完不再使用 |
capture of 'self' with non-Sendable type |
后台闭包捕获普通 class | 改 actor 或 async/await |
mutation of captured var ... in concurrently-executing code |
并发闭包改局部变量 | 任务组汇总结果 |
can not be mutated from a nonisolated context(在 Task.detached 里) |
后台任务直接写主线程状态 | 用 .value 取结果再赋值,或 @concurrent |
stored property ... of 'Sendable'-conforming class ... is mutable |
Sendable class 有 var |
改 actor 或 Mutex |
main actor-isolated default value in a nonisolated context |
非隔离类型里创建主线程对象 | 外层标 @MainActor,或 nonisolated init |
老工程怎么渐进迁移到 Swift 6?
老工程直接切 Swift 6 可能冒出几百条错误。官方迁移指南推荐的顺序是先留在 Swift 5 模式,把检查一级级打开,问题都以警告出现,工程照常能编译和运行。
- 保持 Swift 5 模式,打开严格并发检查。构建设置
SWIFT_STRICT_CONCURRENCY(Strict Concurrency Checking)有minimal、targeted、complete三档,从targeted开始,警告清完再改complete。 - 按模块迁移。先从没有被别的模块依赖的 App target 开始,或者先改被到处引用的公共模块里的全局变量和数据模型,这两类改动能一次消掉大量警告。
- 先如实标注,再重构。迁移指南的原则是 express what is true now。先用
@MainActor、nonisolated、@preconcurrency把现状写清楚,少用nonisolated(unsafe)、@unchecked Sendable,用了的地方留作后续重构的清单。 - 切到 Swift 6。
complete档没有警告了,把这个模块的SWIFT_VERSION改成6。其他模块可以继续留在 5,同一个工程里混用没有问题。
Swift 6.2 起多了几个让代码默认更「单线程」的设置,下表的设置名和说明来自 Apple 的 Build settings reference。
| 构建设置 | 取值 | 作用 |
|---|---|---|
SWIFT_DEFAULT_ACTOR_ISOLATION(Default Actor Isolation) |
nonisolated(默认)或 MainActor |
设为 MainActor 时,没标隔离的代码默认推断为 @MainActor,适合 App target |
SWIFT_APPROACHABLE_CONCURRENCY(Approachable Concurrency) |
YES / NO |
一次打开 NonisolatedNonsendingByDefault、InferIsolatedConformances 等几项新特性 |
SWIFT_UPCOMING_FEATURE_NONISOLATED_NONSENDING_BY_DEFAULT |
YES / NO |
非隔离的 async 函数默认在调用方的隔离域执行,要去后台的显式标 @concurrent |
SWIFT_UPCOMING_FEATURE_INFER_ISOLATED_CONFORMANCES |
YES / NO |
主线程类型的协议一致性默认推断为 @MainActor 一致性,第 5 条的报错会少很多 |
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor 对界面为主的 App 很省事,上面第 2、3、11 条大多直接消失。网络、解析这类要在后台跑的代码,再单独标 nonisolated 或 @concurrent。这几项需要 Swift 6.2 及以上的编译器,终端执行 swift --version 查看版本。
Swift Package 的 target 在 Package.swift 里设置,对应关系如下。defaultIsolation 需要 swift-tools-version 6.2 及以上。
1// swift-tools-version: 6.2
2import PackageDescription
3
4let package = Package(
5 name: "MyKit",
6 targets: [
7 // 已迁移,使用工具版本默认的 Swift 6 模式
8 .target(name: "Networking"),
9 // 还在迁移,留在 Swift 5 模式并打开完整检查
10 .target(
11 name: "LegacyUI",
12 swiftSettings: [
13 .swiftLanguageMode(.v5),
14 .enableUpcomingFeature("StrictConcurrency")
15 ]
16 ),
17 // 界面模块,默认 MainActor 隔离
18 .target(
19 name: "Features",
20 swiftSettings: [
21 .defaultIsolation(MainActor.self)
22 ]
23 )
24 ]
25)
在 KXBuild 里怎么修改并发相关设置?
在 Windows、Linux 上用 KXBuild 时,这些都是工程里的普通构建设置,用工程设置编辑器修改,见工程设置。
- Swift 版本。在 KXBuild 视图中右键 target 选「KXBuild: 编辑目标设置」,General 页 Build 分组的「Swift 版本」就是
SWIFT_VERSION。 - 并发相关设置。切到 Build Settings 页,选 All,在搜索框输入
CONCURRENCY或ACTOR_ISOLATION。打开「显示设置名」能看到上面表里的设置名,底部 Quick Help 显示可选值和说明。用 Levels 视图能看出当前值来自 target、工程还是 xcconfig。 - 保存。Ctrl+S 写回
project.pbxproj,只改动涉及的行。
想先看看打开完整检查后有多少条警告、不想改工程文件的,用 -D 临时覆盖一次构建设置。
1kxbuild build .\MyApp.xcodeproj -D SWIFT_STRICT_CONCURRENCY=complete -D CODE_SIGNING_ALLOWED=NO
完整日志在 VS Code「输出 → KXBuild」,严格并发的错误和警告在问题面板,点一下跳到对应的行。-D 的用法见构建与配置。
常见问题
升级 Xcode 或 Swift 编译器后一定要改成 Swift 6 吗?
不用。新编译器仍然支持 Swift 5 语言模式,SWIFT_VERSION 保持 5 时这些检查最多是警告。可以按模块逐个迁移,同一个工程里 Swift 5 和 Swift 6 的 target 可以共存。
加 @unchecked Sendable 或 nonisolated(unsafe) 消掉报错可以吗?
只在你确实用锁或串行队列保护了所有访问时可以。它们只是让编译器不再检查,数据竞争还在,用错了会在用户手机上出现偶发崩溃或数据错乱。优先用 @MainActor、actor 或改成结构体。
Task { } 和 Task.detached { } 有什么区别?
Task { } 继承当前的隔离,在视图控制器里创建就在主线程执行。Task.detached { } 不继承,在后台执行,不能直接访问主线程的状态。要把耗时工作放到后台,用 Task.detached 返回结果再赋值,Swift 6.2 起也可以用 @concurrent 函数。
SWIFT_DEFAULT_ACTOR_ISOLATION 设成 MainActor 有什么影响?
模块里没标隔离的类型和函数都会默认放到主线程,界面代码的大部分并发报错直接消失。代价是耗时计算也默认在主线程,需要放到后台的函数要显式标 nonisolated 或 @concurrent。这个设置需要 Swift 6.2 及以上的编译器。
delegate 回调报协议一致性跨隔离怎么改?
协议是自己写的、回调总在主线程,给协议标 @MainActor。Swift 6.2 起也可以写 @MainActor PlayerDelegate 这样的隔离一致性。系统或第三方协议,实现方法标 nonisolated,再用 MainActor.assumeIsolated 或 Task { @MainActor in } 回到主线程。