Swift 协议与泛型编程

本文深入讲解 Swift 的协议与泛型体系。从协议定义与协议组合讲起,剖析关联类型、primary associated types 与经典的 PAT 问题,厘清 some 与 any 的语义差异与性能取舍;随后覆盖泛型约束与 where 子句、协议扩展与条件遵循、面向协议编程与面向对象的适用边界、泛型特化的编译优化机制,以及 Swift 6 中 Sendable 对泛型与并发的新约束。最后用策略模式与装饰器模式展示落地方式,并给出可复用的设计检查清单。

开篇

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 的协议与泛型体系可以浓缩成三句话:行为靠协议组合,类型抽象靠泛型,动态性用 any 显式标注。日常写代码时,默认用 some 返回不透明类型以获得静态派发,只在需要异构集合或运行期替换时用 any;带关联类型的协议优先作为泛型约束,必要时才手写类型擦除。协议扩展是复用利器,但要牢记“未在协议声明中列出的方法不会被动态派发”这一铁律。Swift 6 把 Sendable 引入泛型约束体系,让并发安全成为类型签名的一部分——这既是迁移的负担,也是长期收益。掌握这些之后,你写出的 Swift 代码会更接近框架作者的水准:小协议、强约束、零运行时开销。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「iOS 开发」更多文章

  1. Swift Package Manager 与模块化拆分
  2. Core Animation 与 SwiftUI 动画
  3. iOS 安全:Keychain、生物识别与传输安全