可发送类型 Sendable
本教程共 93 篇 · 第 84 篇 · 更新于 2026-08-08 · 约 8 分钟阅读
本节目标:理解 Sendable 协议怎么标记”能安全地在任务和 actor 之间传递”的类型,掌握三类可发送类型的判定规则。
并发里有个绕不开的问题:当你把一个值作为参数传给 actor 方法、或作为任务结果返回时,这个值要离开当前代码、跑到另一个并发域去。如果这个值内部有可变状态又没加保护,跨过去就可能引发数据竞争。Swift 用 Sendable 协议来标记”这种类型可以安全跨域发送”。
1-1 什么是并发域
在一个任务或一个 actor 实例内部,那片”含有可变状态(变量、属性)“的代码区域,叫一个并发域(concurrency domain)。不同任务、不同 actor 是各自独立的并发域。
有些数据没法安全地在并发域之间共享:比如一个含可变属性、又没做串行保护的类,你把它从一个任务传到另一个任务,两边可能同时改它,结果不可预测。相反,有些数据天生安全——比如纯值类型。Swift 用 Sendable 区分这两者。
Note前面几章的例子大多没提 Sendable,因为用的是简单的
Int、String这类值类型,它们天然可发送。一旦你开始传自定义类型、尤其是类,Sendable 就躲不过去了。
1-2 怎样标记一个类型为 Sendable
你通过声明遵循 Sendable 协议来标明”我这类型能安全跨域”。这个协议本身没有任何代码要求,它要的是语义上的保证,由 Swift 在编译期检查:
struct TemperatureReading: Sendable {
var measurement: Int
}
TemperatureReading 是个只含 Int 属性的结构体,所有成员都可发送,于是它可发送。像上面这样不写任何方法,单纯声明遵循即可。
1-3 三类可发送类型
一般来说,一个类型要成为可发送,需满足下面三条之一:
第一,它是值类型,且它的可变状态全部由其他可发送数据组成。比如”所有存储属性都可发送的结构体”,或”关联值都可发送的枚举”。
第二,它没有可变状态,且不可变状态全部由其他可发送数据组成。比如只有只读属性的结构体或类。
第三,它有代码保证自身可变状态的安全。比如被 @MainActor 标记的类,或把属性访问串行化到某个特定线程/队列的类。
这三类覆盖了绝大多数情况。核心思想一句话:要么是不可变、要么是值语义、要么自己把并发保护做足。
1-4 隐式 Sendable 很常见
很多时候你不用手写 : Sendable。Swift 对某些类型自动推断可发送:
- 只含可发送属性的结构体,且不是
public、也没标@usableFromInline,会隐式可发送。 - 关联值都可发送的枚举,同样隐式可发送。
所以下面的结构体哪怕没写遵循,本身就已经可发送了:
struct TemperatureReading {
var measurement: Int
}
// 等价于隐式遵循 Sendable
把可发送的类型传给 actor 方法或作为任务结果,都不会报错。只有当你想显式强调、或用它在需要 Sendable 约束的泛型场景里时,才写上 : Sendable。
Tip隐式合成有个前提:类型定义在非
public的模块内(或没@usableFromInline)。一旦跨模块公开,为了稳定 ABI,Swift 不再隐式合成,你需要显式声明遵循。
1-5 类要成为 Sendable 很难
类因为是引用类型、默认可变,想可发送必须满足第二条或第三条——也就是要么全只读(没有 var,只有 let),要么自带并发保护。
// 只读类:所有属性都是 let,可发送
final class Configuration: Sendable {
let endpoint: String
let timeout: Int
init(endpoint: String, timeout: Int) {
self.endpoint = endpoint
self.timeout = timeout
}
}
如果类有可变属性又不加保护,它不可发送,编译期就会在你想跨域传递它时报警。这是 Swift 在替你拦数据竞争。
1-6 显式声明”不可发送”
反过来,如果你明知某个类型不该跨域,可以显式写”不可用”的遵循,抑制隐式合成,把意图说死:
struct FileDescriptor {
let rawValue: Int
}
@available(*, unavailable)
extension FileDescriptor: Sendable {}
这样任何试图把 FileDescriptor 当 Sendable 传递的代码都会编译失败。这在写库、想明确禁止误用时很有用。
1-7 把可发送数据送进 actor
一个典型用法:自定义一个可发送结构体,作为参数传给 actor 方法,actor 内部把它的值并入自己的状态:
struct TemperatureReading: Sendable {
var measurement: Int
}
actor TemperatureLogger {
private var measurements: [Int] = []
func addReading(from reading: TemperatureReading) {
measurements.append(reading.measurement)
}
}
let logger = TemperatureLogger()
let reading = TemperatureReading(measurement: 45)
await logger.addReading(from: reading)
TemperatureReading 能安全从调用方”发送”进 actor,因为 Swift 确认它可发送。若换成不可发送的类,这行 await logger.addReading 就会编译报错,把隐患挡在编译期。
Warning别为了通过编译就随手给一个含可变状态的类标
: Sendable。这等于向编译器撒谎,运行时会真出数据竞争。只有在你确实满足了三条规则之一时,才标 Sendable。
1-8 Sendable 检查会在哪里拦你
理解 Sendable,最好的方式是知道编译器会在哪些地方替你把关。主要有三处。
第一,把参数传给 actor 方法时,参数类型必须可发送,否则报错。这拦住了”把一个没保护的类实例塞进 actor”的危险。
第二,把值作为任务结果 return 出异步函数时,返回值必须可发送,否则报错。这拦住了”把任务内部可变状态泄露给外部”的危险。
第三,在 Sendable 闭包(如某些并发 API 要求的闭包)里捕获的变量,必须可发送,否则报错。这拦住了”在并发闭包里抓着一个可变对象”的危险。
这三处都在”数据跨越并发域边界”的瞬间检查,正是数据竞争最容易发生的点。Swift 把漏洞堵在编译期,你运行时就少踩坑。
WarningSendable 检查并非万能。它管不住你在运行时故意用
unsafe手段绕过隔离,也管不住某些极隐蔽的间接共享(比如可发送的值内部间接指向可变状态)。它能挡住绝大多数常见错误,但正确的并发设计思维仍是根本。
1-9 小结
Sendable 标记”能安全跨并发域传递”的类型,靠三条规则判定:值类型且成员可发送、无可变状态、或自带并发保护。简单结构体/枚举常获隐式合成;类要可发送基本得全只读或自带隔离;误标会埋下真实竞争。Sendable 与 actor 配合,把”共享可变状态”的安全问题尽量前置到编译期。
下一章讲 @MainActor:Swift 最重要的全局 actor,负责把所有界面相关代码串行到主线程。