Swift并发模型引入Sendable协议后,类型的跨线程传递安全性有了编译期保障。但协议组合(Protocol Composition)这种灵活的类型表达方式,在与Sendable结合时会产生一些容易被忽视的细节。比如你定义了一个protocol Foo & Sendable的组合类型,或者一个协议自身继承了Sendable,那么在泛型约束、函数参数、存储属性等场景下,Sendable约束到底如何传递,组合类型本身是否自动满足Sendable,这些问题都值得深入探讨。

Sendable协议的本质与判定规则
Sendable是一个标记协议(Marker Protocol),它没有方法要求,也不会在运行时产生任何开销。编译器利用它做静态分析,判断一个类型的实例能否安全地从某个并发域移动到另一个并发域。对于struct和enum,如果所有存储属性都是Sendable的,编译器会自动推断其满足Sendable;对于class,默认不满足,除非显式声明遵循并且类是final的、所有存储属性也是Sendable的(或者使用actor隔离)。
需要注意的一个关键点:协议声明本身默认是非Sendable的。也就是说,一个protocol Foo的元类型或者存在类型(existential)默认不能跨并发域传递。只有当协议显式继承Sendable,写作protocol Foo: Sendable,其存在类型any Foo才被认为是Sendable的。这一点在iOS 17/macOS 14之后引入any Foo & Sendable语法之前尤为明显,早期只能通过any Foo配合协议继承来表达双重约束。
此外,Sendable的遵循检查发生在具体类型上,而不是协议本身。协议继承Sendable只是向编译器承诺所有遵循者都必须是Sendable的,真正是否满足还要看具体实现。如果某个struct遵循了继承Sendable的协议,但包含一个非Sendable的class属性,编译器依然会报错,这是约束的传递而非自动豁免。
协议组合中Sendable约束的传递机制
Swift 5.7之后,可以在存在类型中使用组合语法any Foo & Sendable,表示一个值既遵循Foo又是Sendable的。这里的语义是"与"关系:运行时的值必须同时满足两个条件。编译器处理组合类型时,会将组合中的每个协议要求分别检查,因此any Foo & Sendable在需要Sendable值的场景下可以直接使用,例如作为参数传给Task的闭包捕获。
看一个典型例子,两个独立协议组合后跨并发域使用:
protocol Repository {
func fetch(id: String) async throws -> Data
}
// 组合存在类型:同时满足 Repository 和 Sendable
func useRepo(_ repo: any Repository & Sendable) async {
// 可以安全捕获进并发上下文
let result = await Task.detached {
repo.fetch(id: "42")
}.value
}
这段代码能编译通过,因为编译器从组合中读到了Sendable约束。但如果去掉组合中的& Sendable,捕获repo就会触发警告或错误,提示该值可能不安全地跨并发域共享。这正是组合传递Sendable特性的最直接体现:约束写在组合里,就等于写在了这个存在类型上。
在泛型约束中,同样的规则以where子句或协议继承列表的形式表达。例如func process<T: Repository>(repo: T) where T: Sendable,或者更简洁地使用T: Repository & Sendable(Swift 5.7+允许在泛型约束中使用组合写法)。两种写法等价,都要求具体类型同时满足两个协议。对于遵循方来说,具体类型只需要分别声明遵循两个协议,或者让Repository自身继承Sendable,都能满足约束。
常见问题与三种确保Sendable特性的方案
实践中最常见的问题是:协议A没有继承Sendable,但有一个遵循者需要跨线程传递。开发者往往试图在组合位置补救,比如写成any A & Sendable,这确实可行,但要求所有传入的具体类型都额外声明Sendable遵循。如果忘了,就会遇到"conformance to protocol Sendable does not match"之类的错误。
方案一是直接让协议继承Sendable,这是约束最严格的写法,适合所有遵循者天然线程安全的场景:
// 方案一:协议继承,强制所有遵循者满足Sendable
protocol Cache: Sendable {
func get(_ key: String) -> Data?
}
struct MemoryCache: Cache {
// struct 且属性均为值类型,自动满足Sendable
private var storage: [String: Data]
func get(_ key: String) -> Data? { storage[key] }
}
方案二是保持协议纯净,在使用处通过组合约束传递Sendable,适合只有部分场景需要并发传递的情况。方案三是为需要并发的分支定义一个子协议,继承原协议并添加Sendable,这样调用方只需依赖子协议。三种方案的核心差异在于约束施加的位置:协议定义处、使用处、或者中间层。选择时建议优先方案一,因为它把约束显式化,避免了每个使用点都要重复组合语法,也防止后续维护者误用非Sendable的遵循者。
最后要注意@Sendable闭包与组合协议的配合。当闭包被标记为@Sendable后,它捕获的所有值都会被编译器做Sendable检查,捕获一个any A & Sendable的值没问题,但如果捕获的是普通的any A,即便底层具体类型是Sendable的struct,编译器也会拒绝,因为存在类型抹去了具体类型信息,静态检查只能依据协议约束判断。这也是为什么在涉及并发的接口设计上,提前规划好协议是否继承Sendable,比事后补救要省力得多。
编译错误的排查思路
当遇到"non-sendable"相关的诊断时,建议按三个层次排查。第一层看具体类型:class是否非final、是否包含非Sendable的存储属性(比如CLLocationManager这类非Sendable的系统类型)。第二层看协议声明:协议是否继承了Sendable,存在类型是否用了组合写法。第三层看使用位置:泛型约束是否遗漏了Sendable,闭包是否被标记为@Sendable却捕获了受限值。
另外,如果确实无法让类型满足Sendable,可以考虑@unchecked Sendable,它跳过编译器的自动检查,由开发者自行保证线程安全,通常配合锁或串行队列使用。但要谨慎,这是信任声明而非安全保障,一旦内部实现有线程问题,运行时的后果不会被编译器拦截。合理使用协议组合加明确的Sendable约束,配合Swift 6的严格并发检查,才能让并发代码在编译期就获得最大程度的安全保障。
Swift Sendable协议组合并发安全修改时间:2026-09-13 08:14:29