首页 / Swift 编程语言教程 / 泛型约束与 where 子句

Swift 编程语言教程

泛型约束与 where 子句

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

SwiftSwift 编程语言教程generics泛型约束where 子句type constraintEquatable关联类型约束

本节目标:学会给泛型类型参数加约束(必须是某类的子类或遵循某协议),并用 where 子句表达类型之间、关联类型之间的复杂关系。

上一章的泛型函数 swapTwoValuesT 没有任何要求,因为交换操作不需要 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”。DoubleString 都默认遵循 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
}

这里的要求分两类。前两条写在类型参数列表里:C1C2 都得遵循 Container。后两条写在 where 子句里:C1.ItemC2.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 的扩展也一样。给 ContainerstartsWith(_:),要求元素可比较:

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 让你能比较相等,适合查找、去重;ComparableEquatable 基础上支持大小比较,适合排序、求最大最小;Hashable 让类型能当字典的键、能放进 Set,适合做索引和缓存;Numeric 及其子协议(如 AdditiveArithmetic)让元素能加减,适合求和、平均;Codable 让类型能编解码,适合持久化。

选哪个约束,取决于你的泛型逻辑”到底要用到类型的什么能力”。只用到 == 就标 Equatable,别一上来就要求 Comparable——约束越宽,能传入的类型越少,灵活性越差。这条”最小约束原则”值得记牢:刚好够用就好,不要贪多。

还有一点容易忽略:约束写多了,调用处要满足的条件就多,使用方会觉得麻烦。所以设计泛型接口时,先想”最少需要哪些能力”,把约束压到最小,既保护了自己(编译器帮你把关),也方便了别人(能传进来的类型更多)。

补充一句:约束协议之间也能组合使用。你可以在 where 子句里同时要求 ItemEquatableHashable,多条约束用逗号隔开即可。约束越多,能力越强、可传入的类型越窄,这条权衡始终存在,下笔时心里要有数。别为了”显得严谨”把用不上的约束全堆上,那只会把调用方挡在门外。

1-6 约束会被自动推断,别重复写

Swift 很多时候会自动帮你加约束,你不必处处显式写。比如前面提到,泛型代码默认隐含要求类型”可拷贝”(遵循 Copyable),这是隐式约束,你平时感知不到。只有当类型该被拷贝时,才用 ~Copyable 去显式取消这条隐含约束。

理解”隐式约束”能少写很多冗余。多数情况下,常见协议(如 EquatableHashable)会被类型自动遵循,你只管在需要处写下约束即可,不用为了”看起来完整”而把编译器已经隐含的东西再抄一遍。什么时候该显式、什么时候可省略,跟着编译器报错走最准:它说缺约束,你再补。

1-7 小结

泛型约束给占位类型设底线:冒号后写”必须是某类的子类”或”必须遵循某协议”,让泛型代码能放心使用特定能力。where 子句则表达更细的关系——类型参数之间是否相同、关联类型是否遵循某协议或是否等于某具体类型。它既能写在函数/下标上,也能写在扩展上,把新功能精确限定在满足条件的类型才可用。配合约束,泛型从”盲目通用”变成”受控通用”:既能一套逻辑服务多类型,又不丢失类型安全。