# 泛型

Source: https://codewiki.com/zh/swift/generics/

> - **what**: 泛型（generic）用类型参数表示尚未确定的类型，让一份函数或类型定义服务多种具体类型，同时保留这些位置之间的类型关系。
> - **trap**: 无约束的类型参数只能使用所有类型都具备的能力；把泛型改成 `Any`、随意添加约束或混用 `some` 与 `any`，都会改变 API 契约。
> - **fix**: 先写清哪些位置必须是同一类型，再为实现真正使用的能力添加最窄约束，并让调用上下文完成类型推断。

## 是什么，为什么存在

泛型声明把一个或多个具体类型替换成命名的类型参数（type parameter）。调用泛型函数时，调用点提供的实参和预期返回类型共同决定类型参数；创建泛型类型时，尖括号中的类型实参决定其存储和成员签名。`Array` 与 `Array` 使用同一份声明，却是不同的静态类型。

泛型解决的不是“接受任意值”，而是“在未知具体类型时仍保留关系”。函数 `choose(_ left: T, _ right: T) -> T` 说明两个参数和返回值使用同一个 `T`。如果把三个位置都写成 `Any`，编译器便无法保证返回值与输入类型一致，调用方还需要转换。

你会在 Swift 标准库的 `Array`、`Dictionary<Key, Value>`、`Optional` 和 `Result<Success, Failure>` 中持续遇到泛型。应用代码也常用它编写容器、转换函数、数据访问边界和复用算法。只服务一个明确业务类型的代码不必为了形式而泛型化。

类型参数本身没有比较、哈希、编码或领域成员。实现需要某项能力时，声明泛型约束（generic constraint），通常要求类型遵循协议。约束既允许函数体使用协议成员，也会拒绝不满足条件的调用。

泛型抽象应该保留对调用方有意义的类型信息。如果一个参数只被记录或原样传回，可能不需要约束；如果算法只对 `Int` 有业务含义，具体类型反而更诚实。判断标准是契约，不是尖括号越多越复用。

## 工作原理

泛型函数在函数名后声明参数列表，例如 ``。编译器会从每个实参、显式类型标注和周围表达式收集约束，求出一致的具体类型。Swift 不支持在调用处写 `identity(42)` 这种显式函数特化语法；需要更多上下文时，应标注实参或接收结果的类型。

冒号约束表达遵循或继承关系。`<Element: Comparable>` 允许实现使用 `Comparable` 保证的 `<`、`>` 和相等操作，但不会自动授予 `Hashable` 或 `Codable` 的能力。多个要求可以写在泛型参数列表中，也可以放进 `where` 子句。

`where` 子句能约束关联类型，并能声明两个类型表达式必须相同。`Left.Element == Right.Element` 是同类型要求（same-type requirement）：两个集合类型可以不同，但它们的元素必须相同。只有调用点能证明所有要求时，泛型声明才可用。

泛型类型把类型参数纳入类型身份。`Stack` 的 `push` 接收 `String`，`pop` 返回 `String?`；`Stack` 的成员签名则使用 `Int`。实例创建后不能把同一个 `Stack` 改成保存 `Int` 的栈。

协议中的关联类型（associated type）由遵循协议的类型确定。它与泛型参数都表示类型关系，但选择位置不同：泛型类型的使用者写出类型实参，协议的遵循者通过实现确定关联类型。编译器通常可以从属性和方法签名推断该类型，无需显式 `typealias`。

常用语法可以按“谁声明占位类型、谁提供具体类型”来读：

| 语法 | 表达的关系 | 具体类型来自哪里 |
| --- | --- | --- |
| `func load(_: T)` | 一个命名类型参数 | 调用点推断 |
| `struct Box` | 泛型类型身份的一部分 | 创建类型时提供 |
| `T: Hashable` | `T` 必须满足协议 | 编译器验证遵循 |
| `where A.Item == B.Item` | 两个类型表达式相同 | 调用点同时满足 |
| `associatedtype Item` | 协议留下类型槽位 | 遵循类型确定 |

类型推断不是动态类型。编译完成后，每个表达式仍有确定的静态类型；推断只是免去重复书写。错误信息很长时，先在调用边界增加一个有意义的类型标注，通常比在实现内部堆叠转换更容易定位冲突。

### 推断会双向收集信息

类型推断不只从实参流向返回值。赋值目标、闭包参数与返回类型、重载候选和字面量默认类型都会参与同一次求解。例如 `let ids: Set = []` 用左侧类型确定空字面量，而 `let ids = []` 没有足够信息。

检查一次泛型调用时，可以依次标出四类信息来源：

- 每个实参本身的静态类型；
- 接收结果的位置是否声明预期类型；
- 闭包体对参数和结果施加的操作；
- 泛型声明、`where` 子句与所选重载带来的要求。

这些信息必须组成一组一致解。若两个实参分别推出互不兼容的 `T`，编译器不会任选其一，也不会自动插入数值转换。修复应明确业务上需要哪个类型，再在边界执行有名称的转换。

### 约束属于最小能力边界

约束可以出现在泛型声明、方法、受约束扩展和条件遵循上。位置决定整项 API 还是单一能力受限。把约束放得越外层，受影响的无关成员越多。

| 放置位置 | 适合表达的要求 | 对其他成员的影响 |
| --- | --- | --- |
| `struct Cache<Key: Hashable, Value>` | 存储本身始终需要哈希键 | 所有实例都受限 |
| `func contains(...) where Element: Equatable` | 只有该操作需要相等比较 | 其他方法不受限 |
| `extension Box where Value: Codable` | 一组编码成员共享要求 | 仅该扩展的成员可用 |
| `extension Box: Equatable where Value: Equatable` | 遵循关系有条件成立 | 不影响基础类型的创建 |

最窄位置并不总是方法。如果类型的不变量依赖字典键，`Key: Hashable` 属于类型本身；若只有调试导出需要编码，`Codable` 就不应污染核心声明。沿着实际使用约束的表达式向外找到第一个稳定 API 边界即可。

## 示例

下面四个独立程序依次展示泛型函数、泛型类型、`where` 约束和关联类型。当前环境没有本地 Swift 工具链，因此代码块按要求标明未执行；所示输出已另用 Swift 6.3.3 编译器逐段核对。

### 从调用点推断类型参数

`earlier` 的两个参数和返回值共享 `Value`。`Comparable` 约束让函数体可以使用 `<`，而 `repeated` 不检查值，因此不需要额外约束。

<!-- quick -->

```swift
// file: inferred_types.swift
// # not executed here: Swift toolchain is not installed.
func earlier<Value: Comparable>(_ first: Value, _ second: Value) -> Value {
    first < second ? first : second
}

func repeated<Value>(_ value: Value, count: Int) -> [Value] {
    precondition(count >= 0)
    return Array(repeating: value, count: count)
}

let earlierNumber = earlier(42, 17)
let earlierWord = earlier("pear", "apple")
let stages = repeated("ready", count: 3)

print(earlierNumber)
print(earlierWord)
print(stages.joined(separator: ","))
```

```text
17
apple
ready,ready,ready
```

<!-- /quick -->

第一次调用把 `Value` 推断为 `Int`，第二次推断为 `String`。每次调用中的两个参数仍必须采用同一种类型；泛型不会自动把 `Int` 和 `Double` 混成一个共同数值类型。

返回值也参与推断。当实参不能提供足够信息时，可以写 `let result: DesiredType = ...` 或先创建带类型的输入。不要让生成代码用强制转换伪造编译器无法证明的关系。

`precondition` 表达 `count` 的值域要求，这与泛型约束不同。泛型约束检查类型能力，运行时前置条件检查某个具体值。二者不能互相替代。

### 让泛型类型保存一种元素

`Stack` 的存储、`push` 和 `pop` 都使用同一个 `Element`。它的 `map` 方法另外声明 `Output`，因此转换结果可以是另一种栈类型。

```swift
// file: generic_stack.swift
// # not executed here: Swift toolchain is not installed.
struct Stack<Element> {
    private var storage: [Element] = []

    var count: Int { storage.count }

    mutating func push(_ element: Element) {
        storage.append(element)
    }

    mutating func pop() -> Element? {
        storage.popLast()
    }

    func map<Output>(_ transform: (Element) -> Output) -> Stack<Output> {
        var result = Stack<Output>()
        for element in storage {
            result.push(transform(element))
        }
        return result
    }

    func values() -> [Element] { storage }
}

var jobs = Stack<String>()
jobs.push("build")
jobs.push("deploy")
let lengths = jobs.map { $0.count }

print(jobs.pop() ?? "none")
print(lengths.values())
```

```text
deploy
[5, 6]
```

`jobs` 的类型固定为 `Stack`，所以 `push(3)` 会在编译期失败。`map` 没有把原栈改成整数栈，而是创建新的 `Stack`；这种返回类型保留了转换前后的类型关系。

`pop` 返回可选值，因为空栈是有效状态。泛型并不会替 API 决定边界行为。用 `Element?` 表达缺失，比强制解包或返回一个无法适用于所有 `Element` 的哨兵值更准确。

这个实现故意保持接口很小。若加入查找，只应在该操作所在的受约束扩展上要求 `Element: Equatable`，而不是让整个 `Stack` 拒绝不可比较元素。

### 用 where 描述跨类型关系

`sameElements` 接受两种不同集合，只要求元素类型相同且可比较。`unique` 只关心序列元素可哈希，并在保留首次出现顺序的同时去重。

```swift
// file: where_constraints.swift
// # not executed here: Swift toolchain is not installed.
func sameElements<Left: Collection, Right: Collection>(
    _ left: Left,
    _ right: Right
) -> Bool where Left.Element == Right.Element, Left.Element: Equatable {
    left.elementsEqual(right)
}

func unique<Values: Sequence>(_ values: Values) -> [Values.Element]
where Values.Element: Hashable {
    var seen: Set<Values.Element> = []
    return values.filter { seen.insert($0).inserted }
}

let states = ["queued", "running", "done"]
let activeStates = states[0...1]

print(sameElements(["queued", "running"], activeStates))
print(sameElements(["running", "queued"], activeStates))
print(unique(["A", "B", "A", "C", "B"]))
```

```text
true
false
["A", "B", "C"]
```

左侧是 `Array`，右侧是 `ArraySlice`，所以两个集合类型不必相同。`Left.Element == Right.Element` 保证 `elementsEqual` 比较的是同一种元素，`Equatable` 则提供相等比较。

`unique` 使用 `Set` 记录已经出现的元素，因此需要 `Hashable`。这个要求放在函数上而不是输入类型的其他操作上。返回数组保留首次出现顺序，不依赖 `Set` 的迭代顺序。

约束是公开 API 的一部分。以后若实现不再需要哈希，应考虑移除 `Hashable`；保留无用约束会阻止本来能够正确工作的类型调用函数。

### 让遵循类型确定关联类型

`Catalog` 声明了主关联类型（primary associated type） `Item`。尖括号让 `Catalog` 可以约束该关联类型，但它不会把协议本身变成 `Catalog` 结构体那样的泛型类型。

```swift
// file: associated_types.swift
// # not executed here: Swift toolchain is not installed.
struct Product {
    let id: Int
    let name: String
}

protocol Catalog<Item> {
    associatedtype Item
    var items: [Item] { get }
}

struct MemoryCatalog<Item>: Catalog {
    let items: [Item]
}

func first<C: Catalog>(in catalog: C) -> C.Item? {
    catalog.items.first
}

func names(in catalog: some Catalog<Product>) -> [String] {
    catalog.items.map(\.name)
}

let products = MemoryCatalog(items: [
    Product(id: 1, name: "Keyboard"),
    Product(id: 2, name: "Mouse")
])

if let product = first(in: products) {
    print("First: \(product.name)")
}
print(names(in: products).joined(separator: ", "))
```

```text
First: Keyboard
Keyboard, Mouse
```

`MemoryCatalog` 通过 `items` 属性让编译器推断 `Catalog.Item == Product`。`first` 保留任意遵循类型自己的关联类型，返回 `C.Item?`，没有退化成 `Any?`。

参数位置的 `some Catalog` 是一个无需命名类型参数的泛型参数。每次调用仍传入某一个具体遵循类型，函数体知道其 `Item` 是 `Product`。如果两个参数必须共享完全相同的目录类型，就需要给该类型参数命名并在两个位置复用。

主关联类型名称必须对应协议中声明的关联类型。它的主要用途是让 `some Catalog` 或 `any Catalog` 这样的约束更容易书写，不表示调用方在协议声明上填入普通泛型实参。

## 陷阱

### 在函数调用处显式填写类型实参

> **陷阱:** 生成代码常写出 `identity(42)`，仿照支持显式函数特化的语言。Swift 会拒绝这种调用语法，即使 `identity` 确实声明了 `T`。

**修复：**让实参和预期返回类型参与推断，例如 `let value: Int = identity(42)`。如果仍有歧义，给数据边界添加类型标注，不要用 `as!` 掩盖问题。

### 用 Any 代替类型参数

> **陷阱:** `[Any] -> Any` 看似能接受更多输入，却丢掉了元素与返回值之间的关系。函数体只能转换或做运行时检查，调用方也无法从签名得知结果类型。

**修复：**关系存在时使用命名类型参数，例如 `[Element] -> Element?`。只有异构值本来就是数据模型的一部分，而且每种运行时分支都已定义时，才选择 `Any` 或存在类型。

### 把约束加在过宽的位置

> **陷阱:** 为了让一个 `contains` 方法编译，模型可能给整个容器声明 `Element: Equatable & Hashable & Codable`。这样连只读 `count` 的调用方也必须满足无关能力，API 会无谓缩小。

**修复：**把要求放到真正使用它的方法、扩展或条件遵循上，并删除实现没有使用的协议。为受支持和应被拒绝的类型各写一个编译测试。

### 假定一个类型参数能表示异构元素

> **陷阱:** `[T]` 在一次具体使用中只有一种 `T`。要求一个泛型数组同时保存 `Circle` 和 `Rectangle`，不会因为二者都遵循 `Shape` 就自动成立。

**修复：**需要同一具体类型和静态关系时使用 `T: Shape`；需要一个集合在运行时保存不同遵循类型时，明确使用 `[any Shape]` 或合适的枚举。选择会影响可用成员与类型身份，不只是拼写。

### 把 some 与 any 当成可互换语法

> **陷阱:** `some Protocol` 保留一个隐藏的具体类型身份，`any Protocol` 则保存运行时可能不同的遵循值。机械替换可能破坏两个值必须同类型的关系，或让依赖关联类型的成员不再可用。

**修复：**先说明谁选择具体类型，以及调用之间能否变化。调用方提供一种具体类型时用泛型参数；实现方隐藏单一返回类型时用 `some`；确实需要运行时异构性时用 `any`。

<!-- deep -->

## 具体类型由谁选择

泛型参数、`some` 与 `any` 都能让源码只依赖协议能力，但它们保留的信息不同。最有效的判断问题是：具体类型由谁选择，以及两个值之间的同类型关系是否需要跨过 API 边界。

| 形式 | 选择具体类型的一方 | 保留的关系 |
| --- | --- | --- |
| `<T: P>` | 每个调用点 | 所有 `T` 位置相同 |
| 参数位置的 `some P` | 每个调用点 | 该参数有一个未命名具体类型 |
| 返回位置的 `some P` | 函数实现 | 每次返回同一底层类型 |
| `any P` | 运行时值 | 只保证值遵循 `P` |

参数位置的两个独立 `some P` 各自引入一个未命名类型参数，因此不保证彼此同类型。如果实现需要交换、比较或在两者之间传值，应写成命名的 `<T: P>` 并复用 `T`。省略名字只适合实现不需要再次引用该类型的情况。

不透明返回类型由实现选择，并对调用方隐藏名字，但它仍代表一个固定底层类型。存在类型允许每个值装入不同遵循类型，因此适合异构存储和运行时替换。后者会在需要时引入间接层，但不能据此宣称一个固定倍率的性能差异；应在真实调用路径上测量。

## 关联类型形成依赖成员

协议的关联类型依赖于遵循类型的 `Self`。对 `C: Catalog`，`C.Item` 是依赖成员类型；只有确定 `C` 或增加约束后，编译器才能知道 `Item`。这正是泛型函数能够把输入目录与输出元素连接起来的机制。

同类型要求可以连接多个依赖成员，例如 `Left.Item == Right.Item`。遵循类型仍可以不同，只有指定的成员相等。把要求误写成 `Left == Right` 会把契约收紧为完全相同的容器类型，失去原本允许不同实现协作的能力。

主关联类型只提供轻量约束语法。`Catalog` 表示主关联类型 `Item` 被约束为 `Product`；尖括号中的名字必须在协议声明中列出，并对应一个关联类型声明。协议遵循者仍然负责满足这个关系。

存在类型打开后，关联类型信息可能只在局部范围内可用。若函数要把一个值的关联类型传给另一个参数或返回给调用方，命名泛型参数通常能把关系写得更清楚。不要先擦除类型，再用强制转换试图重建编译器已经丢失的证明。

## 特化不是源码契约

Swift 编译器可以针对具体类型特化泛型代码，也可以在不改变可观察语义的前提下选择共享实现。是否特化受优化级别、模块边界、可见性和编译器版本影响。源码不能假定“每个类型一定生成一份机器码”，更不能据此写入固定性能数字。

泛型的首要收益是静态类型关系和复用。性能问题需要在发布构建、目标平台和真实数据上测量，并查看实际调用路径。没有测量时，只能描述可能存在的间接调用、装箱或代码体积权衡，不能把它们写成已发生的成本。

约束也会影响优化机会，但更多约束不等于更快。无用约束首先改变的是谁能调用 API，并可能传播到上层声明。先写最小正确契约，再用分析工具确认瓶颈，最后才考虑为热点提供具体重载或调整边界。

## 重载与推断需要清楚边界

重载集合也参与泛型求解。两个重载都能接受同一实参时，编译器会比较哪一个更具体；复杂闭包、默认参数和返回类型上下文可能让这个选择难以预测。公共 API 不应依赖读者猜测细微的重载优先级。

若两个泛型重载表达不同业务语义，使用不同的基本名称通常更清楚。若它们只是为更具体的类型提供优化路径，行为和失败规则必须与通用版本一致。调用测试应覆盖静态类型不同但运行时数据相同的情况。

错误发生在长链式表达式中时，把每一阶段赋给带类型的局部常量。这样既能指出是哪一段缺少上下文，也能阻止上游改动意外选择另一重载。诊断完成后可以保留这些名称，因为它们常常也说明了数据转换的含义。

## 泛型边界应保持可读

一个公开签名若同时暴露许多类型参数、嵌套关联类型和同类型要求，调用方很难判断真正的不变量。先寻找能否用现有标准库协议表达输入，再为业务关系命名。类型别名能缩短重复拼写，但不会减少底层复杂度。

编译器诊断变得难读时，可以把长表达式拆成带类型的中间值，或把约束移到命名辅助函数。这些修改给求解器和读者都提供局部边界。随意加入 `Any`、`as!` 或额外协议只是把问题推迟到运行时或上层 API。

库演进还要求区分语义约束和实现约束。若公开函数声明 `Element: Hashable`，以后即使实现不再使用哈希，旧调用方也已经把它视为契约的一部分。对外暴露前，应逐项回答调用方为什么需要知道这项要求。

测试泛型 API 时，不要只运行一个 `Int` 快乐路径。选择两个结构不同但都满足约束的类型，验证关系确实来自协议；再保留一个不能编译的示例，证明边界拒绝了什么。运行时测试验证行为，编译测试验证类型契约，两者职责不同。

<!-- /deep -->

[检查点: swift/generics](https://codewiki.com/zh/swift/generics/#checkpoint)

## 延伸阅读

- [Swift 语言指南：泛型](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/generics/)
- [Swift 语言指南：协议](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/protocols/)
- [Swift 6.3 发布说明](https://www.swift.org/blog/swift-6.3-released/)
