跳到主要内容

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 模式,把检查一级级打开,问题都以警告出现,工程照常能编译和运行。

  1. 保持 Swift 5 模式,打开严格并发检查。构建设置 SWIFT_STRICT_CONCURRENCY(Strict Concurrency Checking)有 minimal、targeted、complete 三档,从 targeted 开始,警告清完再改 complete。
  2. 按模块迁移。先从没有被别的模块依赖的 App target 开始,或者先改被到处引用的公共模块里的全局变量和数据模型,这两类改动能一次消掉大量警告。
  3. 先如实标注,再重构。迁移指南的原则是 express what is true now。先用 @MainActor、nonisolated、@preconcurrency 把现状写清楚,少用 nonisolated(unsafe)、@unchecked Sendable,用了的地方留作后续重构的清单。
  4. 切到 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 } 回到主线程。