Swift 6.1 于 2025 年 3 月 31 日发布,其中一个实用变化是允许把 nonisolated 写在类型和扩展上。过去如果一组声明都不依赖某个全局执行器,可能需要在多个成员上重复标注;现在可以在更接近设计意图的层级表达隔离规则。

nonisolated 不是“关闭并发检查”

nonisolated 表示声明不隔离到 actor 或全局 actor,并不表示它可以不受约束地读写共享可变状态。跨并发域传递的值仍然要满足 Sendable 要求,数据竞争也仍然是错误。

一个只包含不可变、可发送数据的请求值很适合显式声明为非隔离类型:

nonisolated struct SearchQuery: Sendable, Equatable {
    let text: String
    let page: Int

    init(text: String, page: Int = 1) {
        self.text = text.trimmingCharacters(in: .whitespacesAndNewlines)
        self.page = max(1, page)
    }
}

它不访问 UI,也没有隐藏的可变引用。构造、比较和编码这类工作不需要跳到主执行器。相比给每个初始化器和计算属性分别加标注,类型级声明更容易审查。

把 UI 隔离和纯值分开

UI 模型仍然可以明确属于主执行器:

@MainActor
final class SearchViewModel {
    private(set) var results: [String] = []
    private let client: SearchClient

    init(client: SearchClient) {
        self.client = client
    }

    func search(_ query: SearchQuery) async throws {
        let values = try await client.fetch(query)
        results = values
    }
}

这里的 SearchQuery 可以在任意并发上下文创建,SearchClient 负责异步 I/O,只有把结果写入 UI 状态时才需要主执行器。若把所有辅助类型也隔离到 MainActor,后台任务会产生没有业务价值的执行器切换。

扩展级 nonisolated 适合一组纯转换或格式化能力。例如某个类型因所在模块的默认规则获得了全局 actor 隔离,但它的一组值语义帮助方法并不需要该隔离,可以在扩展边界集中表达。使用前仍要检查这些方法访问的存储;只要其中一个成员读取了 actor 隔离状态,就不能为了消除诊断而整组标为非隔离。

迁移时先读诊断

不要搜索项目中的并发错误,然后批量插入 nonisolated。先判断声明属于哪一类:UI 状态应保留 @MainActor;独占可变状态可能需要 actor;不可变值通常适合 Sendable;真正与执行器无关的 API 才考虑 nonisolated

还要警惕 @unchecked Sendable。它把线程安全证明转交给开发者,却不会让代码自动安全。若一个引用类型内部有锁,应把锁策略、受保护字段和回调重入规则写清楚并测试;若没有这样的证明,就不应使用 unchecked 逃开编译器。

在大型项目中,先对一个模块启用更严格检查并记录诊断类别。把真正的隔离设计修改与第三方库适配分开提交,代码审查才能看出某个 nonisolated 是领域事实还是临时兼容措施。临时措施应附带删除条件,避免它在未来失去上下文后变成永久例外。

用边界验证设计

并发迁移测试应覆盖跨边界行为:从后台任务创建查询;取消网络请求;同时发起多个查询;确保旧响应不会覆盖新结果。编译器负责发现许多静态隔离问题,测试负责验证取消、排序和领域规则。

Swift 6.1 的类型级与扩展级 nonisolated 让代码少了一些重复标注,更重要的是让隔离意图出现在正确层级。它最适合纯值和纯行为,不适合用来压制尚未理解的并发诊断。