首页 / Swift 编程语言教程 / 协议中的关联类型

Swift 编程语言教程

协议中的关联类型

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

SwiftSwift 编程语言教程associatedtype关联类型协议类型擦除where 约束

本节目标:理解 associatedtype 如何用”占位类型”让协议描述容器式能力,并学会给关联类型加约束、处理类型擦除。

上一章的协议,要求里的类型是写死的,比如 var area: Double。可有些协议天生就绕不开”某种还没确定的类型”。最典型的就是”容器”:一个能装东西的盒子,装的到底是 Int 还是 String,协议定义时并不知道。Swift 用”关联类型”解决这种场景。

1-1 关联类型是什么

关联类型(associated type)是协议里的一个”占位名”,代表某种将来才确定下来的类型。具体用哪个类型,等某个具体类型来遵循协议时再补上。声明用 associatedtype 关键字。

它和普通泛型参数的区别是:普通泛型写在函数或类型名字后面的尖括号里;关联类型写在协议内部,是协议的一部分,由遵循者去”填空”。

打个比方:你定了一份”收纳盒”协议,要求”能往里放东西、能数有几个、能按编号取出来”。至于盒子里装的是书还是玩具,协议不规定,只用个占位词”物品”指代。等到你拿一个”书盒”来遵循协议时,“物品”才被明确成”书”。

1-2 用 Container 协议看关联类型

下面这份 Container 协议用 Item 作为关联类型:

protocol Container {
    associatedtype Item
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

它规定了容器必须具备的三项能力:能用 append(_:) 加一个 Item;有 count 属性知道数量;能用下标按 Int 取出第 iItem。注意协议全程没说 Item 到底是什么——它只是个占位。

让一个具体类型来遵循,就要把 Item 落实。下面这个 IntStackItem 指定成 Int

struct IntStack: Container {
    var items: [Int] = []
    mutating func push(_ item: Int) { items.append(item) }
    mutating func pop() -> Int { items.removeLast() }
    mutating func append(_ item: Int) { self.push(item) }
    var count: Int { items.count }
    subscript(i: Int) -> Int { items[i] }
}

这里 append 的参数、subscript 的返回类型都写成了 Int,于是 Item 自然就是 Int。协议不关心你内部怎么存,只关心那三项能力齐不齐。

1-3 类型推断会替你填上关联类型

Swift 很聪明:只要遵循者把协议要求的方法签名都写清楚,编译器能反推出关联类型是什么,你不必显式写 typealias Item = Int。上面 IntStack 里那行 typealias 删掉也照常编译。

泛型类型遵循时也一样自然。下面让第 70 章会写的泛型 Stack<Element> 去遵循 Container,把 Element 直接当作 Item

struct Stack<Element>: Container {
    var items: [Element] = []
    mutating func push(_ item: Element) { items.append(item) }
    mutating func pop() -> Element { items.removeLast() }
    mutating func append(_ item: Element) { self.push(item) }
    var count: Int { items.count }
    subscript(i: Int) -> Element { items[i] }
}

因为 append 的参数是 Elementsubscript 返回 Element,Swift 自动推断 Item 就是 Element。连标准库的 Array 也能这样”搭便车”:它本身就有 append、有 count、有 Int 下标,所以只需一句空扩展声明遵循即可:

extension Array: Container {}

Array 现有的方法让编译器推断出 Item 是数组元素类型,从此任意数组都能当 Container 用。

关联类型带来的最大好处,是让协议能描述”容器""迭代器""比较器”这类”本身就依赖某个未知类型”的抽象。没有它,你要么把类型写死、失去通用性,要么退回用 Any 做类型擦除、丢掉编译期安全。关联类型恰好站在中间:对外保持通用,在遵循时又被编译器锁定成具体类型,安全与灵活兼得。这也是为什么标准库里大量协议(如 CollectionIteratorProtocol)都建立在关联类型之上——它们要服务的,正是”元素类型未定”的集合行为。

1-4 给关联类型加约束

关联类型也能加约束,要求”填空”的那个类型必须满足某些条件。比如要求容器里的元素必须可比较相等:

protocol Container {
    associatedtype Item: Equatable
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

这下,想遵循这个版本的 Container,其 Item 类型必须遵循 Equatable。约束把”空泛的能力”收紧成”可比较的能力”,从而能在协议内部安全地使用 ==

协议还能”引用自己”。下面 SuffixableContainer 继承 Container,又声明了一个关联类型 Suffix,要求 Suffix 自己也得是 SuffixableContainer,且它的 Item 必须和容器的 Item 相同:

protocol SuffixableContainer: Container {
    associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
    func suffix(_ size: Int) -> Suffix
}

这种”协议里出现协议自身”的写法,用来表达递归式的能力关系。where Suffix.Item == Item 正是泛型 where 子句用在关联类型上的形式。

1-5 关联类型上的 where 子句

除了在声明关联类型时直接写约束,还可以在协议里给关联类型追加 where 子句,表达更复杂的关系。比如给 Container 加一个”迭代器”关联类型,要求它遍历出来的元素类型必须和容器的 Item 一致:

protocol Container {
    associatedtype Item
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }

    associatedtype Iterator: IteratorProtocol where Iterator.Element == Item
    func makeIterator() -> Iterator
}

继承协议时也能在协议声明处加约束。例如要求 Item 必须可比较:

protocol ComparableContainer: Container where Item: Comparable { }

这些 where 子句让协议对”关联类型之间的关系”描述得又准又灵活,是泛型与协议结合的关键语法。

1-6 类型擦除解决了什么

关联类型有个现实麻烦:带关联类型的协议,不能直接当成”值类型”来存。你不能写 let box: Container——因为编译器不知道 Item 是谁,无法在编译期确定内存布局和可用成员。这跟普通协议能写成 let x: SomeProtocol 不一样。

解决思路叫”类型擦除”(type erasure):包一层具体的盒子类型,让它去遵循协议,把内部那个不确定的类型”藏”起来,对外只暴露协议要求的固定接口。标准库里 AnySequenceAnyIterator 就是这么做的——Any 前缀的类型专门用来”擦掉”底层真实类型,让你可以把不同 Item 的容器统一塞进同一种容器里传递。

实际写代码时,多数情况你不需要自己手写类型擦除:优先用泛型约束(<T: Container>)或 some / any 这类存在类型来表达意图。只有当确实需要把”多种不同具体类型的遵循者”放进同一个变量或数组,而又不想让调用方知道具体类型时,才考虑引入一个擦除类型的小包装。理解它,能帮你看懂标准库里那些 Any 开头的类型为何存在。

1-7 小结

关联类型用 associatedtype 在协议里占个”类型坑位”,等遵循者再来填具体类型,让协议能描述容器式、泛型式的能力。Swift 通常能自动推断填什么,不必手写 typealias。给关联类型加约束(含 where 子句)能把能力收紧到可比较、可迭代等具体范畴。带关联类型的协议不能直接当值类型使用,需要时可用”类型擦除”的盒子类型把底层类型藏起来。下章我们正式进入泛型,看占位类型在普通函数和类型里怎么用。