Swift 6.1 的 nonisolated 类型:减少并发隔离样板代码
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 让代码少了一些重复标注,更重要的是让隔离意图出现在正确层级。它最适合纯值和纯行为,不适合用来压制尚未理解的并发诊断。