首页 / Swift 编程语言教程 / 可发送类型 Sendable

Swift 编程语言教程

可发送类型 Sendable

本教程共 93 篇 · 第 84 篇 · 更新于 2026-08-08 · 约 8 分钟阅读

SwiftSwift 编程语言教程并发Sendable可发送类型并发域数据竞争协议

本节目标:理解 Sendable 协议怎么标记”能安全地在任务和 actor 之间传递”的类型,掌握三类可发送类型的判定规则。

并发里有个绕不开的问题:当你把一个值作为参数传给 actor 方法、或作为任务结果返回时,这个值要离开当前代码、跑到另一个并发域去。如果这个值内部有可变状态又没加保护,跨过去就可能引发数据竞争。Swift 用 Sendable 协议来标记”这种类型可以安全跨域发送”。

1-1 什么是并发域

在一个任务或一个 actor 实例内部,那片”含有可变状态(变量、属性)“的代码区域,叫一个并发域(concurrency domain)。不同任务、不同 actor 是各自独立的并发域。

有些数据没法安全地在并发域之间共享:比如一个含可变属性、又没做串行保护的类,你把它从一个任务传到另一个任务,两边可能同时改它,结果不可预测。相反,有些数据天生安全——比如纯值类型。Swift 用 Sendable 区分这两者。

Note

前面几章的例子大多没提 Sendable,因为用的是简单的 IntString 这类值类型,它们天然可发送。一旦你开始传自定义类型、尤其是类,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 {}

这样任何试图把 FileDescriptorSendable 传递的代码都会编译失败。这在写库、想明确禁止误用时很有用。

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 把漏洞堵在编译期,你运行时就少踩坑。

Warning

Sendable 检查并非万能。它管不住你在运行时故意用 unsafe 手段绕过隔离,也管不住某些极隐蔽的间接共享(比如可发送的值内部间接指向可变状态)。它能挡住绝大多数常见错误,但正确的并发设计思维仍是根本。

1-9 小结

Sendable 标记”能安全跨并发域传递”的类型,靠三条规则判定:值类型且成员可发送、无可变状态、或自带并发保护。简单结构体/枚举常获隐式合成;类要可发送基本得全只读或自带隔离;误标会埋下真实竞争。Sendable 与 actor 配合,把”共享可变状态”的安全问题尽量前置到编译期。

下一章讲 @MainActor:Swift 最重要的全局 actor,负责把所有界面相关代码串行到主线程。