第一次在 Swift 代码中看到尖括号 <T> 时,你可能会停顿一下。函数名旁边多了一个大写字母,打开文档后,还会看到类似 func map<T>(_ transform: (Element) -> T) -> [T] 的签名。泛型是 Swift 进阶的入门关卡。标准库几乎所有内容(包括 Array、Dictionary 和 Optional)都是用泛型实现的;跨过这道门槛后,才能开始读懂库代码。
这是进阶系列的第 2 篇。本文总结泛型解决的问题、何时需要约束和 where 子句,以及它对性能的影响。
泛型解决的问题——拒绝在代码重复与类型安全之间二选一
在没有泛型的世界里,尝试编写一个交换两个值的函数,问题马上就会暴露出来。
为 Int 编写的版本不能用于 String。按类型复制代码又会增加重复。因此,人们会想到一个能接收任意类型的盒子,也就是 Swift 中的 Any。但一旦使用 Any,类型信息就消失了。每次取出都要做类型转换,把 Int 放进去却按 String 取出的错误会通过编译,最终在运行时崩溃。
总之,过去只有两个选择:没有重复但有风险的 Any,或安全但重复的按类型复制。泛型拒绝这种二选一。
func swapValues<T>(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}
<T>表示一个稍后填入类型的空位。调用时,T 会确定为具体类型,编译器再用这个确定的类型检查全部代码。像 swapValues(&intA, &strB) 这样的类型不匹配调用会产生编译错误。代码只有一份,但类型检查按类型进行,这就是泛型的本质。
类型也同样适用。声明 struct Stack<Element> 后,Stack<Int> 和 Stack<String> 会成为不同类型,编译器会阻止你把字符串放进 Int 栈。Array 正是这种结构;Optional 也如可选值一篇所见,是名为 enum Optional<Wrapped> 的泛型 enum。换句话说,你早就在每天使用泛型。
约束——从“任意类型”到“具备这些能力的类型”
默认情况下,T 这个空位可以是任意类型。但这也意味着它什么都做不了:编译器不了解 T,因此无法比较、输出或相加。
func largest<T>(_ items: [T]) -> T? {
items.max(by: <) // 编译错误 — T无法保证可比较
}
这时就轮到约束登场了。写成 <T: Comparable>,就附加了“T 只能是遵循 Comparable 的类型”这一条件,于是可以对 T 使用 <。
func largest<T: Comparable>(_ items: [T]) -> T? {
items.max()
}
约束不是损失,而是一种交换:缩小可接收的类型范围,却增加对这些类型可执行的操作。协议一篇中讲过的“能力组合”,正是在这里与泛型相遇,因为约束使用的就是协议。
条件复杂时,可以使用 where 子句。位置不同,含义相同;当需要约束类型参数的关联类型时,where 就是必需的。
// Element仅比较 Equatable相同的集合
func allEqual<C: Collection>(_ items: C) -> Bool where C.Element: Equatable {
guard let first = items.first else { return true }
return items.allSatisfy { $0 == first }
}
C.Element指的是集合所包含的元素类型,即关联类型(associated type)。这是后续篇章会重点讨论的主题;这里先记住 where 是施加这一条件的位置即可。
在实践中,约束应当只加到必要程度。只需要 Comparable 的函数如果同时要求 Hashable,可用类型反而会减少。约束列表应准确对应函数体实际使用的能力。
性能——编译器消除了抽象的成本
面对“泛型不是会变慢吗?”这个问题,可以用 Swift 的代表性优化来回答:特化。
原则上,泛型函数不知道会传入什么类型,因此需要在运行时携带类型信息并间接执行。但如果编译器能看到调用点,就会为 swapValues<Int> 单独生成专用版本。最终机器码与手写 Int 版本相同。使用泛型抽象并不意味着支付运行时成本;编译器会剥离抽象,生成具体代码。这正是哲学一篇所说的“零成本抽象”的代表案例。
在同一模块内,这项优化通常运行良好;跨越模块边界后会受到限制(取决于库的发布形式)。实践结论是,不必因为担心性能而避开泛型;真正的瓶颈应通过测量找出。提前优化的问题已有另一篇文章讨论。
这里自然会出现一个问题:“如果接收协议类型(例如 items: [Comparable]),它和泛型有什么区别?”这是下一篇的主题。泛型是编译时确定类型的静态多态,而协议类型(existential)是在运行时混合类型的动态多态。下一篇还会解释 Swift 为什么用 some 和 any 明确区分二者。
什么时候创建泛型——实践判断标准
阅读泛型和亲自设计泛型是两个层次,因此这里总结创建泛型时的判断标准。
当同一逻辑只改变类型却第二次出现时,就是信号。 一开始就想着“以后可能会有其他类型”而使用泛型,大多属于过度设计。遵循 YAGNI(You Aren’t Gonna Need It——需要时再做):先从具体类型开始,真正出现重复时再泛化。
与其用于领域概念,不如用于结构和算法。 缓存、分页响应、栈等与内容无关的结构,正是泛型的主场。比如 APIResponse<User>、Cache<ImageKey, UIImage>。相反,强行将订单支付等特定领域逻辑泛型化,只会让签名更难理解。
当签名复杂度超过使用收益时,就是该退一步的信号。 如果有三四个类型参数,where 子句又占三行,就该问问调用它的同事是否看得懂。Progressive Disclosure 一篇中的原则同样适用:复杂性应由声明端吸收,不能泄漏到使用端。
总结
- 泛型拒绝在无重复复用与类型安全之间二选一。代码只有一份,但类型检查按类型进行。
<T>是类型空位声明;约束(T: Comparable和 where 子句)缩小空位范围,同时扩大可执行的操作。- 得益于特化,泛型通常能达到与手写具体代码相同的性能。
- 设计标准:出现第二次重复时再泛化,用于结构和算法;如果签名复杂度超过收益,就停下来。
下一篇介绍泛型的兄弟语法,也是现代 Swift 中最容易混淆的语法:some 和 any。我们将深入探究 some View 的真相,以及 existential 的成本。

![[Swift 进阶 #2] 用泛型消除重复与风险 封面图](/assets/images/posts/7d69dc2a-8c58-4422-956e-940173ebaf99/1.jpg)