开篇
Swift 常被称为“面向协议编程(Protocol-Oriented Programming, POP)”的语言。Apple 在 2015 年的 WWDC 演讲中明确提出这个说法,用意是纠正从 Objective-C 继承来的“一切皆类”思维:在 Swift 里,共享行为靠协议,共享数据靠值类型,而不是靠类继承树。
但真正写起来,协议与泛型有一堆容易混淆的点:
some和any到底差在哪,什么时候该用哪个;- 关联类型(associated type)让协议不能直接当类型用,怎么绕;
- 协议扩展里的方法为什么有时候不生效(静态派发 vs 动态派发);
- 泛型代码的性能到底和写死类型差多少;
- Swift 6 的
Sendable为什么会让一堆泛型代码报错。
这篇文章把这些串成一条线:先讲协议的表达能力,再讲泛型如何做类型抽象,最后落到设计取舍与性能。前置阅读建议 Swift 语言基础与现代语法 ,错误处理相关的泛型技巧可参考 Swift 可选类型与错误处理 。
一、协议基础与协议组合
1.1 定义与遵循
协议声明一组要求(属性、方法、初始化器、下标、关联类型),任何类型都可以遵循:
protocol Identifiable {
associatedtype ID: Hashable
var id: ID { get }
}
protocol Persistable {
func save() throws
static func load(id: String) throws -> Self
}
struct Article: Identifiable, Persistable {
let id: UUID
var title: String
func save() throws {
try DiskStore.write(self, key: id.uuidString)
}
static func load(id: String) throws -> Article {
try DiskStore.read(key: id)
}
}
协议可以要求 { get }(只读)或 { get set }(读写),也可以要求 static 成员。要求 Self 或关联类型的协议不能直接用作类型,这就是后面要讲的 PAT 问题。
1.2 协议组合
用 & 组合多个协议,表达“同时满足多个约束”:
func render(_ item: some Identifiable & Persistable) -> String {
"\(item.id)"
}
// 作为参数类型的组合(existential)
func saveAll(_ items: [any Identifiable & Persistable]) throws {
for item in items { try item.save() }
}
协议组合是替代“为每种组合定义一个空协议”的正确做法。比如需要“可比较且可哈希”时,写 Hashable & Comparable,而不是新建一个 HashableComparable 协议。
二、关联类型
2.1 associatedtype 的作用
关联类型让协议在不知道具体类型的前提下表达“某类型”,由遵循者通过类型推断或 typealias 确定:
protocol Container {
associatedtype Element
var count: Int { get }
mutating func append(_ element: Element)
subscript(index: Int) -> Element { get }
}
struct Stack<Element>: Container {
private var storage: [Element] = []
var count: Int { storage.count }
mutating func append(_ element: Element) { storage.append(element) }
subscript(index: Int) -> Element { storage[index] }
}
Stack 的 Element 直接来自泛型参数,编译器自动满足 Container.Element。
2.2 primary associated types
Swift 5.7 引入 primary associated types,允许在协议名后用尖括号指定关联类型,从而把“约束”写在类型位置:
protocol Collection<Element> {
associatedtype Element
// ...
}
// 现在可以这样写
func firstElement<C: Collection<String>>(_ c: C) -> String? { c.first }
声明时用 <Element> 标出 primary,就能在 any 与泛型约束中直接写 Collection<String>、any Collection<Int>。这让泛型代码的签名更贴近直觉,也减少了“必须为每个关联类型再套一层泛型”的样板。
2.3 PAT 问题
“PAT”是 Protocol with Associated Type 的缩写。带关联类型的协议不能直接作为类型使用:
// 编译错误:Protocol 'Container' can only be used as a generic constraint
// var box: Container
// 正确:作为泛型约束
func sum<C: Container>(_ c: C) -> Int where C.Element == Int { ... }
// 或用 any 擦除
var box: any Container = Stack<Int>()
用 any Container 可以当类型用,但会丢失具体类型信息、且无法访问关联类型相关的 API(比如 append 的签名里含 Element,通过 any 调用会有类型擦除限制)。经典的解法是“类型擦除(type erasure)”——手写一个 AnyContainer 包装类型:
struct AnyContainer<Element>: Container {
private let _count: () -> Int
private let _append: (Element) -> Void
private let _get: (Int) -> Element
init<C: Container>(_ base: C) where C.Element == Element {
_count = { base.count }
_append = { var b = base; b.append($0) }
_get = { base[$0] }
}
var count: Int { _count() }
mutating func append(_ element: Element) { _append(element) }
subscript(index: Int) -> Element { _get(index) }
}
在 Swift 5.7 之后,很多场景可以用 any 替代手写擦除类型;但当你需要“保留具体类型、参与泛型约束”时,手写擦除仍不可替代。
三、some 与 any
3.1 opaque type 与 some
some P 表示“某个遵循 P 的具体类型,但调用方不需要知道是哪个”。它是不透明类型,编译期就能确定真实类型:
func makeShape(isCircle: Bool) -> some Shape {
isCircle ? Circle(radius: 1) : Square(side: 2) // 必须返回同一具体类型
}
注意约束:some 返回值的所有分支必须是同一个具体类型(上例会报错,因为 Circle 与 Square 不同),这是不透明类型与 any 的关键差别。
SwiftUI 的 body 属性返回 some View 就是典型用法:它让框架能针对具体类型做优化,同时把冗长的泛型嵌套类型名隐藏掉。
3.2 existential 与 any
any P 表示“任意遵循 P 的类型”,是存在类型,运行期可能对应多种类型,通过 witness table 做动态派发:
var shapes: [any Shape] = [Circle(radius: 1), Square(side: 2)] // 可以混装
for shape in shapes { print(shape.area) }
Swift 5.7 起要求显式写 any(以前可以省略),目的是让“动态派发的成本”在代码里可见。any 会带来一层间接(boxing + witness table),且无法访问 Self 或关联类型相关的 API。
3.3 some 与 any 对比
| 维度 | some P(不透明) | any P(存在) |
|---|---|---|
| 类型确定性 | 编译期确定单一具体类型 | 运行期可为多种类型 |
| 能否混装不同类型 | 否 | 是 |
| 派发方式 | 静态派发,可内联 | 动态派发(witness table) |
| 性能 | 接近具体类型 | 有间接开销 |
访问 Self/关联类型 API | 可以 | 受限 |
| 典型用途 | 函数返回值、SwiftUI body | 异构集合、依赖注入、擦除 |
选择原则:返回单一具体类型用 some,需要装多种类型用 any。默认优先 some,因为它性能更好且不丢类型信息。
四、泛型约束与 where 子句
泛型参数可以加约束,where 子句能表达更复杂的关系:
func merge<C1: Collection, C2: Collection>(_ a: C1, _ b: C2) -> [C1.Element]
where C1.Element == C2.Element, C1.Element: Comparable {
(Array(a) + Array(b)).sorted()
}
也可以约束关联类型之间的关系:
extension Container where Element: Equatable {
func contains(_ element: Element) -> Bool {
(0..<count).contains { self[$0] == element }
}
}
extension Sequence where Element: Numeric {
var sum: Element { reduce(.zero, +) }
}
where 子句还支持“同类型”约束(C1.Element == C2.Element)与“遵循关系”约束(T: Sequence)。这些约束在编译期做检查,运行时零成本。
五、协议扩展与条件遵循
协议扩展可以为遵循者提供默认实现,这是 POP 的核心工具:
protocol Greeter {
var name: String { get }
func greet() -> String
}
extension Greeter {
func greet() -> String { "你好,\(name)" } // 默认实现
func greetLoudly() -> String { greet().uppercased() } // 便利方法
}
struct Person: Greeter { let name: String } // 无需实现 greet
struct Dog: Greeter {
let name: String
func greet() -> String { "汪!我是 \(name)" } // 覆盖默认实现
}
条件遵循(conditional conformance)让“满足额外条件时才遵循某协议”成为可能:
extension Stack: Equatable where Element: Equatable {
static func == (lhs: Stack, rhs: Stack) -> Bool { lhs.storage == rhs.storage }
}
extension Stack: Encodable where Element: Encodable {}
标准库大量使用这一技巧:Array 只在 Element: Equatable 时才 Equatable,Optional 只在 Wrapped: Equatable 时才 Equatable。
5.1 静态派发 vs 动态派发
这是 POP 最容易踩的坑。协议扩展中未在协议声明里要求的方法,通过 any 调用时走静态派发:
protocol Shape { func area() -> Double }
extension Shape {
func area() -> Double { 0 } // 在协议里声明了,动态派发
func describe() -> String { "shape" } // 未在协议里声明,静态派发
}
struct Circle: Shape {
func area() -> Double { 3.14 }
func describe() -> String { "circle" }
}
let s: any Shape = Circle()
print(s.area()) // 3.14 —— 动态派发,走到 Circle 的实现
print(s.describe()) // shape —— 静态派发,用的是扩展里的默认实现
describe() 没有出现在 protocol Shape 的声明里,因此对 any Shape 类型调用时编译器按 Shape 的扩展实现派发,子类型的“覆盖”不会被调用。规则:想让方法可被覆盖,必须在协议声明里列出它。
六、面向协议编程与面向对象的取舍
| 维度 | 面向协议(POP) | 面向对象(继承) |
|---|---|---|
| 复用单位 | 协议 + 扩展(水平组合) | 基类(垂直继承) |
| 类型 | 值类型友好 | 主要面向引用类型 |
| 多继承 | 支持多个协议 | 单一父类 |
| 派发 | 默认静态,可优化 | 默认动态(vtable) |
| 风险 | 过度抽象、协议爆炸 | 脆弱基类、钻石继承 |
| 测试 | 易 mock(协议注入) | 需子类化或运行时替换 |
实践建议:
- 行为抽象用协议,数据共享用值类型;
- 协议按“能力”划分(
Equatable、Codable、Cacheable),不要按“是什么”划分(ArticleProtocol); - 优先小协议 + 组合,避免大而全的“上帝协议”;
- 需要共享存储/初始化逻辑时,值类型用组合,引用类型才考虑继承。
七、泛型特化与性能
Swift 的泛型在编译期通过**特化(specialization)**优化:当编译器能确定具体类型时,会生成专用版本并内联,性能接近手写具体类型。这依赖全模块优化(Whole Module Optimization,WMO),Release 构建默认开启。
func firstIndex<T: Equatable>(of value: T, in array: [T]) -> Int? {
array.firstIndex(of: value)
}
let index = firstIndex(of: 42, in: [1, 2, 42]) // 编译器特化为 Int 版本
影响因素:
- 跨模块调用如果未开启 WMO 或库未暴露
@inlinable,可能无法特化,退化为动态派发; any会引入 witness table 间接调用,无法特化;- 值类型泛型参数在特化后可完全栈分配。
要验证是否特化,可以用 Instruments 的 Time Profiler 看是否有大量 witness 相关符号,或用 swiftc -O -emit-assembly 观察是否生成了具体类型的符号。库作者应给关键泛型函数标 @inlinable 并 @usableFromInline 暴露依赖的内部符号,让下游能特化。
八、Swift 6 的 Sendable 与并发约束
Swift 6 的严格并发检查把 Sendable 变成泛型编程的常见约束:
// 要求元素可跨 actor 传递
func process<T: Sendable>(_ items: [T]) async -> [T] { items }
// 泛型容器在元素 Sendable 时才 Sendable
struct Box<T> { var value: T }
extension Box: Sendable where T: Sendable {}
要点:
- 值类型且所有成员
Sendable时,编译器可自动推断Sendable; class默认不Sendable,需要final+ 不可变存储属性,或标@unchecked Sendable并自行保证线程安全;- 闭包作为泛型参数传递时,可能需要
@Sendable标注; @MainActor类型与Sendable的关系:actor 隔离的类型可以跨隔离域安全传递引用。
actor Cache<Key: Hashable & Sendable, Value: Sendable> {
private var storage: [Key: Value] = [:]
func set(_ value: Value, for key: Key) { storage[key] = value }
func get(_ key: Key) -> Value? { storage[key] }
}
迁移建议:先给核心数据模型加 Sendable 约束,再逐模块开启严格并发;@unchecked Sendable 只作为临时手段,必须配注释说明不变量。
九、设计模式在 Swift 的落地
9.1 策略模式
用协议替代抽象基类,用值类型实现具体策略:
protocol DiscountStrategy {
func discount(for amount: Decimal) -> Decimal
}
struct NoDiscount: DiscountStrategy {
func discount(for amount: Decimal) -> Decimal { 0 }
}
struct PercentageOff: DiscountStrategy {
let rate: Decimal
func discount(for amount: Decimal) -> Decimal { amount * rate }
}
struct Checkout {
let strategy: any DiscountStrategy
func total(_ amount: Decimal) -> Decimal { amount - strategy.discount(for: amount) }
}
这里用 any DiscountStrategy 是因为策略在运行期可能变化;如果策略是编译期固定的(比如通过泛型参数注入),可以写成 Checkout<S: DiscountStrategy> 以获取静态派发的性能。
9.2 装饰器模式
用协议 + 包装类型层层叠加行为,值类型让每层都是独立副本:
protocol DataSource {
func read() throws -> String
}
struct FileSource: DataSource {
let path: String
func read() throws -> String { try String(contentsOfFile: path, encoding: .utf8) }
}
struct LoggingSource: DataSource {
let wrapped: any DataSource
func read() throws -> String {
print("读取开始")
defer { print("读取结束") }
return try wrapped.read()
}
}
struct CachingSource: DataSource {
let wrapped: any DataSource
private static var cache: [String: String] = [:]
func read() throws -> String { try wrapped.read() }
}
装饰器在 Swift 里比继承更自然:每层只关心自己的职责,组合顺序一目了然,测试时替换任意一层即可。
常见坑清单
- 在协议扩展里定义“便利方法”却期望子类型覆盖,实际走静态派发导致不生效;
- 用
any装异构集合后又想访问关联类型相关 API,编译不过; - 给
some返回值的分支返回不同具体类型,编译报错(应改用any或统一类型); - 协议按“类型名”划分(
UserServiceProtocol)而非按能力划分,导致协议爆炸; - 泛型库函数未标
@inlinable,下游无法特化,性能不达预期; @unchecked Sendable到处用,实际并未保证线程安全;- 条件遵循写错约束(
where Element: Equatable写成where Self: Equatable); - 手写类型擦除类型时忘记处理
mutating语义,导致修改丢失。
相关阅读
- Swift 语言基础与现代语法 — 值类型、协议思维与属性包装器,是理解 POP 的前置。
- Swift 可选类型与错误处理
—
rethrows与泛型错误传播的配合方式。 - Swift ARC 与内存管理
— 存在类型的装箱与引用计数开销,理解
any的性能成本。
小结
Swift 的协议与泛型体系可以浓缩成三句话:行为靠协议组合,类型抽象靠泛型,动态性用 any 显式标注。日常写代码时,默认用 some 返回不透明类型以获得静态派发,只在需要异构集合或运行期替换时用 any;带关联类型的协议优先作为泛型约束,必要时才手写类型擦除。协议扩展是复用利器,但要牢记“未在协议声明中列出的方法不会被动态派发”这一铁律。Swift 6 把 Sendable 引入泛型约束体系,让并发安全成为类型签名的一部分——这既是迁移的负担,也是长期收益。掌握这些之后,你写出的 Swift 代码会更接近框架作者的水准:小协议、强约束、零运行时开销。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。