泛型约束与 where 子句
本教程共 93 篇 · 第 71 篇 · 更新于 2026-08-08 · 约 9 分钟阅读
本节目标:学会给泛型类型参数加约束(必须是某类的子类或遵循某协议),并用
where子句表达类型之间、关联类型之间的复杂关系。
上一章的泛型函数 swapTwoValues 对 T 没有任何要求,因为交换操作不需要 T 会什么。但很多泛型逻辑天生就”要求类型会点什么”:想查找元素,类型得能比较相等;想算平均值,元素得是数值。泛型约束(constraint)就是用来设这条底线的。
1-1 为什么需要约束
看一个想写成泛型、却编不过的函数:在数组里找某个值的下标。
func findIndex<T>(of valueToFind: T, in array: [T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
问题出在 value == valueToFind 这一行。不是所有类型都能用 == 比较——你自己定义的复杂类型,Swift 可猜不出”相等”意味着什么。所以编译器报错:它对”任意 T”没法保证 == 可用。
解法:要求 T 必须遵循标准库的 Equatable 协议,而 Equatable 正好保证类型支持 == 和 !=。
1-2 约束语法
约束写在类型参数名后面,用冒号隔开,放在尖括号的类型参数列表里。约束可以是一个类(要求是该类的子类),也可以是一个协议或协议组合(要求遵循它)。
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// 函数体
}
上面 T: SomeClass 表示 T 必须是 SomeClass 的子类;U: SomeProtocol 表示 U 必须遵循 SomeProtocol。语法和”协议约束”那套冒号写法一致,记住”冒号 = 必须是 / 必须遵循”即可。
回到查找下标的例子,加上约束就通过了:
func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25]) // nil
let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"]) // 2
T: Equatable 读作”任何遵循 Equatable 的类型 T”。Double 和 String 都默认遵循 Equatable,所以这个函数现在谁都能安全用。
1-3 where 子句:更细的关系
类型约束能管”每个类型参数本身要满足什么”,但有时候你要表达的是”类型参数之间”或”关联类型之间”的关系。这时用泛型 where 子句:以 where 开头,后面写约束,放在函数体左大括号之前。
下面这个函数检查两个容器是否装了完全相同、顺序也相同的元素:
func allItemsMatch<C1: Container, C2: Container>
(_ someContainer: C1, _ anotherContainer: C2) -> Bool
where C1.Item == C2.Item, C1.Item: Equatable {
if someContainer.count != anotherContainer.count {
return false
}
for i in 0..<someContainer.count {
if someContainer[i] != anotherContainer[i] {
return false
}
}
return true
}
这里的要求分两类。前两条写在类型参数列表里:C1 和 C2 都得遵循 Container。后两条写在 where 子句里:C1.Item 和 C2.Item 必须是同一类型,而且这个 Item 要遵循 Equatable(这样 != 才可用)。
重点:两个容器不必是同一类型,只要装的”元素类型相同”且”元素可比较”即可。这正是约束组合的力量——它能让不同容器类型互相比较。
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
var arrayOfStrings = ["uno", "dos", "tres"]
if allItemsMatch(stackOfStrings, arrayOfStrings) {
print("全部匹配。")
}
Stack<String> 和 [String] 类型不同,却都满足 Container 且元素都是 String,因此能一起传进 allItemsMatch。
1-4 带 where 子句的扩展
where 子句也能用在扩展上。下面给泛型 Stack 加一个 isTop(_:) 方法,但只对”元素可比较”的栈生效:
extension Stack where Element: Equatable {
func isTop(_ item: Element) -> Bool {
guard let topItem = items.last else {
return false
}
return topItem == item
}
}
为什么非要 where Element: Equatable?因为方法体里用了 ==。但 Stack 本身没要求元素可比较,直接写 == 会编译失败。加上 where 约束后,这个方法只在”元素可比较”的栈上才出现,编译才合法。
if stackOfStrings.isTop("tres") {
print("栈顶是 tres。")
}
如果你对一个”元素不可比较”的栈调 isTop,编译器会拒绝——因为那个方法对它根本不存在。
给协议写带 where 的扩展也一样。给 Container 加 startsWith(_:),要求元素可比较:
extension Container where Item: Equatable {
func startsWith(_ item: Item) -> Bool {
return count >= 1 && self[0] == item
}
}
where 还可以要求 Item 是某个具体类型,而不仅是遵循协议:
extension Container where Item == Double {
func average() -> Double {
var sum = 0.0
for index in 0..<count {
sum += self[index]
}
return sum / Double(count)
}
}
print([1260.0, 1200.0, 98.6, 37.0].average()) // 648.9
这条扩展只对”装 Double 的容器”生效,于是内部能放心做浮点运算。
1-5 常用的约束协议
写约束时,有几份标准库协议最常被当作”底线”。Equatable 让你能比较相等,适合查找、去重;Comparable 在 Equatable 基础上支持大小比较,适合排序、求最大最小;Hashable 让类型能当字典的键、能放进 Set,适合做索引和缓存;Numeric 及其子协议(如 AdditiveArithmetic)让元素能加减,适合求和、平均;Codable 让类型能编解码,适合持久化。
选哪个约束,取决于你的泛型逻辑”到底要用到类型的什么能力”。只用到 == 就标 Equatable,别一上来就要求 Comparable——约束越宽,能传入的类型越少,灵活性越差。这条”最小约束原则”值得记牢:刚好够用就好,不要贪多。
还有一点容易忽略:约束写多了,调用处要满足的条件就多,使用方会觉得麻烦。所以设计泛型接口时,先想”最少需要哪些能力”,把约束压到最小,既保护了自己(编译器帮你把关),也方便了别人(能传进来的类型更多)。
补充一句:约束协议之间也能组合使用。你可以在 where 子句里同时要求 Item 既 Equatable 又 Hashable,多条约束用逗号隔开即可。约束越多,能力越强、可传入的类型越窄,这条权衡始终存在,下笔时心里要有数。别为了”显得严谨”把用不上的约束全堆上,那只会把调用方挡在门外。
1-6 约束会被自动推断,别重复写
Swift 很多时候会自动帮你加约束,你不必处处显式写。比如前面提到,泛型代码默认隐含要求类型”可拷贝”(遵循 Copyable),这是隐式约束,你平时感知不到。只有当类型不该被拷贝时,才用 ~Copyable 去显式取消这条隐含约束。
理解”隐式约束”能少写很多冗余。多数情况下,常见协议(如 Equatable、Hashable)会被类型自动遵循,你只管在需要处写下约束即可,不用为了”看起来完整”而把编译器已经隐含的东西再抄一遍。什么时候该显式、什么时候可省略,跟着编译器报错走最准:它说缺约束,你再补。
1-7 小结
泛型约束给占位类型设底线:冒号后写”必须是某类的子类”或”必须遵循某协议”,让泛型代码能放心使用特定能力。where 子句则表达更细的关系——类型参数之间是否相同、关联类型是否遵循某协议或是否等于某具体类型。它既能写在函数/下标上,也能写在扩展上,把新功能精确限定在满足条件的类型才可用。配合约束,泛型从”盲目通用”变成”受控通用”:既能一套逻辑服务多类型,又不丢失类型安全。