# 泛型
*泛型代碼*讓你能根據自定義的需求,編寫出適用于任意類型的、靈活可復用的函數及類型。你可避免編寫重復的代碼,而是用一種清晰抽象的方式來表達代碼的意圖。
泛型是 Swift 最強大的特性之一,很多 Swift 標準庫是基于泛型代碼構建的。實際上,即使你沒有意識到,你也一直在*語言指南*中使用泛型。例如,Swift 的 `Array` 和 `Dictionary` 都是泛型集合。你可以創建一個 `Int` 類型數組,也可創建一個 `String` 類型數組,甚至可以是任意其他 Swift 類型的數組。同樣,你也可以創建一個存儲任意指定類型的字典,并對該類型沒有限制。
## 泛型解決的問題 {#the-problem-that-generics-solve}
下面是一個標準的非泛型函數 `swapTwoInts(_:_:)`,用來交換兩個 `Int` 值:
```swift
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
```
這個函數使用輸入輸出參數(`inout`)來交換 `a` 和 `b` 的值,具體請參考 [輸入輸出參數](./06_Functions.md#in-out-parameters)。
`swapTwoInts(_:_:)` 函數將 `b` 的原始值換成了 `a`,將 `a` 的原始值換成了 `b`,你可以調用這個函數來交換兩個 `Int` 類型變量:
```swift
var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// 打印“someInt is now 107, and anotherInt is now 3”
```
`swapTwoInts(_:_:)` 函數很實用,但它只能作用于 `Int` 類型。如果你想交換兩個 `String` 類型值,或者 `Double` 類型值,你必須編寫對應的函數,類似下面 `swapTwoStrings(_:_:)` 和 `swapTwoDoubles(_:_:)` 函數:
```swift
func swapTwoStrings(_ a: inout String, _ b: inout String) {
let temporaryA = a
a = b
b = temporaryA
}
func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
let temporaryA = a
a = b
b = temporaryA
}
```
你可能注意到了,`swapTwoInts(_:_:)`、`swapTwoStrings(_:_:)` 和 `swapTwoDoubles(_:_:)` 函數體是一樣的,唯一的區別是它們接受的參數類型(`Int`、`String` 和 `Double`)。
在實際應用中,通常需要一個更實用更靈活的函數來交換兩個任意類型的值,幸運的是,泛型代碼幫你解決了這種問題。(這些函數的泛型版本已經在下面定義好了。)
> 注意
>
> 在上面三個函數中,`a` 和 `b` 類型必須相同。如果 `a` 和 `b` 類型不同,那它們倆就不能互換值。Swift 是類型安全的語言,所以它不允許一個 `String` 類型的變量和一個 `Double` 類型的變量互換值。試圖這樣做將導致編譯錯誤。
## 泛型函數 {#generic-functions}
泛型函數可適用于任意類型,下面是函數 `swapTwoInts(_:_:)` 的泛型版本,命名為 `swapTwoValues(_:_:)`:
```swift
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}
```
`swapTwoValues(_:_:)` 和 `swapTwoInts(_:_:)` 函數體內容相同,它們只在第一行不同,如下所示:
```swift
func swapTwoInts(_ a: inout Int, _ b: inout Int)
func swapTwoValues<T>(_ a: inout T, _ b: inout T)
```
泛型版本的函數使用`占位符`類型名(這里叫做 `T` ),而不是 *實際*類型名(例如 `Int`、`String` 或 `Double`),`占位符`類型名并不關心 `T` 具體的類型,但它要求 `a` 和` b` 必須是相同的類型,`T` 的實際類型由每次調用 `swapTwoValues(_:_:)` 來決定。
泛型函數和非泛型函數的另外一個不同之處在于這個泛型函數名(`swapTwoValues(_:_:)`)后面跟著占位類型名(`T`),并用尖括號括起來(`<T>`)。這個尖括號告訴 Swift 那個 `T` 是 `swapTwoValues(_:_:)` 函數定義內的一個占位類型名,因此 Swift 不會去查找名為 `T `的實際類型。
`swapTwoValues(_:_:)` 函數現在可以像 `swapTwoInts(_:_:)` 那樣調用,不同的是它能接受兩個任意類型的值,條件是這兩個值有著相同的類型。`swapTwoValues(_:_:)` 函數被調用時,`T ` 所代表的類型都會由傳入的值的類型推斷出來。
在下面的兩個例子中,`T` 分別代表 ` Int` 和 `String`:
```swift
var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt 現在是 107,anotherInt 現在是 3
var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString 現在是“world”,anotherString 現在是“hello”
```
> 注意
>
> 上面定義的 `swapTwoValues(_:_:)` 函數是受 `swap(_:_:)` 函數啟發而實現的。后者存在于 Swift 標準庫,你可以在你的應用程序中使用它。如果你在代碼中需要類似 `swapTwoValues(_:_:)` 函數的功能,你可以使用已存在的 `swap(_:_:)` 函數。
## 類型參數 {#type-parameters}
上面 `swapTwoValues(_:_:)` 例子中,占位類型 `T` 是一個類型參數的例子,類型參數指定并命名一個占位類型,并且緊隨在函數名后面,使用一對尖括號括起來(例如 `<T>`)。
一旦一個類型參數被指定,你可以用它來定義一個函數的參數類型(例如 `swapTwoValues(_:_:)` 函數中的參數 `a` 和 `b`),或者作為函數的返回類型,還可以用作函數主體中的注釋類型。在這些情況下,類型參數會在函數調用時被實際類型所替換。(在上面的 `swapTwoValues(_:_:)` 例子中,當函數第一次被調用時,`T` 被 `Int` 替換,第二次調用時,被 `String` 替換。)
你可提供多個類型參數,將它們都寫在尖括號中,用逗號分開。
## 命名類型參數 {#naming-type-parameters}
大多情況下,類型參數具有描述下的名稱,例如字典 `Dictionary<Key, Value>` 中的 `Key` 和 `Value` 及數組 `Array<Element>` 中的 `Element`,這能告訴閱讀代碼的人這些參數類型與泛型類型或函數之間的關系。然而,當它們之間沒有有意義的關系時,通常使用單個字符來表示,例如 `T`、`U`、`V`,例如上面演示函數 `swapTwoValues(_:_:)` 中的 `T`。
> 注意
>
> 請始終使用大寫字母開頭的駝峰命名法(例如 `T` 和 `MyTypeParameter`)來為類型參數命名,以表明它們是占位類型,而不是一個值。
## 泛型類型 {#generic-types}
除了泛型函數,Swift 還允許自定義*泛型類型*。這些自定義類、結構體和枚舉可以適用于*任意類型*,類似于 `Array` 和 `Dictionary`。
本節將向你展示如何編寫一個名為 `Stack`(棧)的泛型集合類型。棧是值的有序集合,和數組類似,但比數組有更嚴格的操作限制。數組允許在其中任意位置插入或是刪除元素。而棧只允許在集合的末端添加新的元素(稱之為入棧)。類似的,棧也只能從末端移除元素(稱之為出棧)。
> 注意
>
> 棧的概念已被 `UINavigationController` 類用來構造視圖控制器的導航結構。你通過調用 `UINavigationController` 的 `pushViewController(_:animated:)` 方法來添加新的視圖控制器到導航棧,通過 `popViewControllerAnimated(_:)` 方法來從導航棧中移除視圖控制器。每當你需要一個嚴格的“后進先出”方式來管理集合,棧都是最實用的模型。
下圖展示了入棧(push)和出棧(pop)的行為:

1. 現在有三個值在棧中。
2. 第四個值被壓入到棧的頂部。
3. 現在棧中有四個值,最近入棧的那個值在頂部。
4. 棧中最頂部的那個值被移除出棧。
5. 一個值移除出棧后,現在棧又只有三個值了。
下面展示如何編寫一個非泛型版本的棧,以 `Int` 型的棧為例:
```swift
struct IntStack {
var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
}
```
這個結構體在棧中使用一個名為 `items` 的數組屬性來存儲值。棧提供了兩個方法:`push(_:)` 和 `pop()`,用來向棧中壓入值以及從棧中移除值。這些方法被標記為 `mutating`,因為它們需要修改結構體的 `items` 數組。
上面的 `IntStack` 結構體只能用于 `Int` 類型。不過,可以定義一個泛型 `Stack` 結構體,從而能夠處理任意類型的值。
下面是相同代碼的泛型版本:
```swift
struct Stack<Element> {
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}
```
注意,`Stack` 基本上和 `IntStack` 相同,只是用占位類型參數 `Element` 代替了實際的 `Int` 類型。這個類型參數包裹在緊隨結構體名的一對尖括號里(<`Element`>)。
`Element` 為待提供的類型定義了一個占位名。這種待提供的類型可以在結構體的定義中通過 `Element` 來引用。在這個例子中,`Element` 在如下三個地方被用作占位符:
+ 創建 `items` 屬性,使用 `Element` 類型的空數組對其進行初始化。
+ 指定 `push(_:)` 方法的唯一參數 `item` 的類型必須是 `Element` 類型。
+ 指定 `pop()` 方法的返回值類型必須是 `Element` 類型。
由于 `Stack` 是泛型類型,因此可以用來創建適用于 Swift 中任意有效類型的棧,就像 `Array` 和 `Dictionary` 那樣。
你可以通過在尖括號中寫出棧中需要存儲的數據類型來創建并初始化一個 `Stack` 實例。例如,要創建一個 `String` 類型的棧,可以寫成 `Stack<String>()`:
```swift
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// 棧中現在有 4 個字符串
```
下圖展示了 `stackOfStrings` 如何將這四個值壓棧:

移除并返回棧頂部的值“cuatro”,即出棧:
```swift
let fromTheTop = stackOfStrings.pop()
// fromTheTop 的值為“cuatro”,現在棧中還有 3 個字符串
```
下圖展示了如何將頂部的值出棧:

## 泛型擴展 {#extending-a-generic-type}
當對泛型類型進行擴展時,你并不需要提供類型參數列表作為定義的一部分。原始類型定義中聲明的類型參數列表在擴展中可以直接使用,并且這些來自原始類型中的參數名稱會被用作原始定義中類型參數的引用。
下面的例子擴展了泛型類型 `Stack`,為其添加了一個名為 `topItem` 的只讀計算型屬性,它將會返回當前棧頂元素且不會將其從棧中移除:
```swift
extension Stack {
var topItem: Element? {
return items.isEmpty ? nil : items[items.count - 1]
}
}
```
`topItem` 屬性會返回 `Element` 類型的可選值。當棧為空的時候,`topItem` 會返回 `nil`;當棧不為空的時候,`topItem` 會返回 `items` 數組中的最后一個元素。
注意:這個擴展并沒有定義類型參數列表。相反的,`Stack` 類型已有的類型參數名稱 `Element`,被用在擴展中來表示計算型屬性 `topItem` 的可選類型。
計算型屬性 `topItem` 現在可以用來訪問任意 `Stack` 實例的頂端元素且不移除它:
```swift
if let topItem = stackOfStrings.topItem {
print("The top item on the stack is \(topItem).")
}
// 打印“The top item on the stack is tres.”
```
泛型類型的擴展,還可以包括類型擴展需要額外滿足的條件,從而對類型添加新功能,這一部分將在 [具有泛型 Where 子句的擴展](#extensions-with-a-generic-where-clause) 中進行討論。
## 類型約束 {#type-constraints}
`swapTwoValues(_:_:)` 函數和 `Stack` 適用于任意類型。不過,如果能對泛型函數或泛型類型中添加特定的*類型約束*,這將在某些情況下非常有用。類型約束指定類型參數必須繼承自指定類、遵循特定的協議或協議組合。
例如,Swift 的 `Dictionary` 類型對字典的鍵的類型做了些限制。在 [字典的描述](./04_Collection_Types.md#dictionaries) 中,字典鍵的類型必須是可哈希(hashable)的。也就是說,必須有一種方法能夠唯一地表示它。字典鍵之所以要是可哈希的,是為了便于檢查字典中是否已經包含某個特定鍵的值。若沒有這個要求,字典將無法判斷是否可以插入或替換某個指定鍵的值,也不能查找到已經存儲在字典中的指定鍵的值。
這個要求通過 `Dictionary` 鍵類型上的類型約束實現,它指明了鍵必須遵循 Swift 標準庫中定義的 `Hashable` 協議。所有 Swift 的基本類型(例如 `String`、`Int`、`Double` 和 `Bool`)默認都是可哈希的。如何讓自定義類型遵循 `Hashable` 協議,可以查看文檔 [遵循 Hashable 協議](https://developer.apple.com/documentation/swift/hashable#2849490)。
當自定義泛型類型時,你可以定義你自己的類型約束,這些約束將提供更為強大的泛型編程能力。像 `可哈希(hashable)` 這種抽象概念根據它們的概念特征來描述類型,而不是它們的具體類型。
### 類型約束語法 {#type-constraint-syntax}
在一個類型參數名后面放置一個類名或者協議名,并用冒號進行分隔,來定義類型約束。下面將展示泛型函數約束的基本語法(與泛型類型的語法相同):
```swift
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// 這里是泛型函數的函數體部分
}
```
上面這個函數有兩個類型參數。第一個類型參數 `T` 必須是 `SomeClass` 子類;第二個類型參數 `U` 必須符合 `SomeProtocol` 協議。
### 類型約束實踐 {#type-constraints-in-action}
這里有個名為 `findIndex(ofString:in:)` 的非泛型函數,該函數的功能是在一個 `String` 數組中查找給定 `String` 值的索引。若查找到匹配的字符串,`findIndex(ofString:in:)` 函數返回該字符串在數組中的索引值,否則返回 `nil`:
```swift
func findIndex(ofString valueToFind: String, in array: [String]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
```
`findIndex(ofString:in:)` 函數可以用于查找字符串數組中的某個字符串值:
```swift
let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]
if let foundIndex = findIndex(ofString: "llama", in: strings) {
print("The index of llama is \(foundIndex)")
}
// 打印“The index of llama is 2”
```
如果只能查找字符串在數組中的索引,用處不是很大。不過,你可以用占位類型 `T` 替換 `String` 類型來寫出具有相同功能的泛型函數 `findIndex(_:_:)`。
下面展示了 `findIndex(ofString:in:)` 函數的泛型版本 `findIndex(of:in:)`。請注意這個函數返回值的類型仍然是 `Int?`,這是因為函數返回的是一個可選的索引數,而不是從數組中得到的一個可選值。需要提醒的是,這個函數無法通過編譯,原因將在后面說明:
```swift
func findIndex<T>(of valueToFind: T, in array:[T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
```
上面所寫的函數無法通過編譯。問題出在相等性檢查上,即 "`if value == valueToFind`"。不是所有的 Swift 類型都可以用等式符(`==`)進行比較。例如,如果你自定義類或結構體來描述復雜的數據模型,對于這個類或結構體而言,Swift 無法明確知道“相等”意味著什么。正因如此,這部分代碼無法保證適用于任意類型 `T`,當你試圖編譯這部分代碼時就會出現相應的錯誤。
不過,所有的這些并不會讓我們無從下手。Swift 標準庫中定義了一個 `Equatable` 協議,該協議要求任何遵循該協議的類型必須實現等式符(`==`)及不等符(`!=`),從而能對該類型的任意兩個值進行比較。所有的 Swift 標準類型自動支持 `Equatable` 協議。
遵循 `Equatable` 協議的類型都可以安全地用于 `findIndex(of:in:)` 函數,因為其保證支持等式操作符。為了說明這個事情,當定義一個函數時,你可以定義一個 `Equatable` 類型約束作為類型參數定義的一部分:
```swift
func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
```
`findIndex(of:in:)` 類型參數寫做 `T: Equatable`,也就意味著“任何符合 `Equatable` 協議的類型 `T`”。
`findIndex(of:in:)` 函數現在可以成功編譯了,并且適用于任何符合 `Equatable` 的類型,如 `Double` 或 `String`:
```swift
let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25])
// doubleIndex 類型為 Int?,其值為 nil,因為 9.3 不在數組中
let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"])
// stringIndex 類型為 Int?,其值為 2
```
## 關聯類型 {#associated-types}
定義一個協議時,聲明一個或多個關聯類型作為協議定義的一部分將會非常有用。關聯類型為協議中的某個類型提供了一個占位符名稱,其代表的實際類型在協議被遵循時才會被指定。關聯類型通過 `associatedtype` 關鍵字來指定。
### 關聯類型實踐 {#associated-types-in-action}
下面例子定義了一個 `Container` 協議,該協議定義了一個關聯類型 `Item`:
```swift
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
```
`Container` 協議定義了三個任何遵循該協議的類型(即容器)必須提供的功能:
+ 必須可以通過 `append(_:)` 方法添加一個新元素到容器里。
+ 必須可以通過 `count` 屬性獲取容器中元素的數量,并返回一個 Int 值。
+ 必須可以通過索引值類型為 `Int` 的下標檢索到容器中的每一個元素。
該協議沒有指定容器中元素該如何存儲以及元素類型。該協議只指定了任何遵從 `Container` 協議的類型必須提供的三個功能。遵從協議的類型在滿足這三個條件的情況下,也可以提供其他額外的功能。
任何遵從 `Container` 協議的類型必須能夠指定其存儲的元素的類型。具體來說,它必須確保添加到容器內的元素以及下標返回的元素類型是正確的。
為了定義這些條件,`Container` 協議需要在不知道容器中元素的具體類型的情況下引用這種類型。`Container` 協議需要指定任何通過 `append(_:)` 方法添加到容器中的元素和容器內的元素是相同類型,并且通過容器下標返回的元素的類型也是這種類型。
為此,`Container` 協議聲明了一個關聯類型 `Item`,寫作 `associatedtype Item`。協議沒有定義 `Item` 是什么,這個信息留給遵從協議的類型來提供。盡管如此,`Item` 別名提供了一種方式來引用 `Container` 中元素的類型,并將之用于 `append(_:)` 方法和下標,從而保證任何 `Container` 的行為都能如預期。
這是前面非泛型版本 `IntStack` 類型,使其遵循 `Container` 協議:
```swift
struct IntStack: Container {
// IntStack 的原始實現部分
var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
// Container 協議的實現部分
typealias Item = Int
mutating func append(_ item: Int) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Int {
return items[i]
}
}
```
`IntStack` 結構體實現了 `Container` 協議的三個要求,其原有功能也不會和這些要求相沖突。
此外,`IntStack` 在實現 `Container` 的要求時,指定 `Item` 為 `Int` 類型,即 `typealias Item = Int`,從而將 `Container` 協議中抽象的 `Item` 類型轉換為具體的 `Int` 類型。
由于 Swift 的類型推斷,實際上在 `IntStack` 的定義中不需要聲明 `Item` 為 `Int`。因為 `IntStack` 符合 `Container` 協議的所有要求,Swift 只需通過 `append(_:)` 方法的 `item` 參數類型和下標返回值的類型,就可以推斷出 `Item` 的具體類型。事實上,如果你在上面的代碼中刪除了 `typealias Item = Int` 這一行,一切也可正常工作,因為 Swift 清楚地知道 `Item` 應該是哪種類型。
你也可以讓泛型 `Stack` 結構體遵循 `Container` 協議:
```swift
struct Stack<Element>: Container {
// Stack<Element> 的原始實現部分
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
// Container 協議的實現部分
mutating func append(_ item: Element) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Element {
return items[i]
}
}
```
這一次,占位類型參數 `Element` 被用作 `append(_:)` 方法的 `item` 參數和下標的返回類型。Swift 可以據此推斷出 `Element` 的類型即是 `Item` 的類型。
### 擴展現有類型來指定關聯類型 {#extending-an-existing-type-to-specify-an-associated-type}
[在擴展添加協議一致性](./21_Protocols.md#adding-protocol-conformance-with-an-extension) 中描述了如何利用擴展讓一個已存在的類型遵循一個協議,這包括使用了關聯類型協議。
Swift 的 `Array` 類型已經提供 `append(_:)` 方法,`count` 屬性,以及帶有 `Int` 索引的下標來檢索其元素。這三個功能都符合 `Container` 協議的要求,也就意味著你只需聲明 `Array` 遵循`Container` 協議,就可以擴展 Array,使其遵從 Container 協議。你可以通過一個空擴展來實現這點,正如通過擴展采納協議中的描述:
```swift
extension Array: Container {}
```
`Array` 的 `append(_:)` 方法和下標確保了 Swift 可以推斷出 `Item` 具體類型。定義了這個擴展后,你可以將任意 `Array` 當作 Container 來使用。
### 給關聯類型添加約束 {#adding-constraints-to-an-associated-type}
你可以在協議里給關聯類型添加約束來要求遵循的類型滿足約束。例如,下面的代碼定義了 `Container` 協議, 要求關聯類型 `Item` 必須遵循 `Equatable` 協議:
```swift
protocol Container {
associatedtype Item: Equatable
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
```
要遵守 `Container` 協議,`Item` 類型也必須遵守 `Equatable` 協議。
### 在關聯類型約束里使用協議 {#using-a-protocol-in-its-associated-types-constraints}
協議可以作為它自身的要求出現。例如,有一個協議細化了 `Container` 協議,添加了一個` suffix(_:)` 方法。`suffix(_:)` 方法返回容器中從后往前給定數量的元素,并把它們存儲在一個 `Suffix` 類型的實例里。
```swift
protocol SuffixableContainer: Container {
associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
func suffix(_ size: Int) -> Suffix
}
```
在這個協議里,`Suffix` 是一個關聯類型,就像上邊例子中 `Container` 的 `Item` 類型一樣。`Suffix` 擁有兩個約束:它必須遵循 `SuffixableContainer` 協議(就是當前定義的協議),以及它的 `Item` 類型必須是和容器里的 `Item` 類型相同。`Item` 的約束是一個 `where` 分句,它在下面 [具有泛型 Where 子句的擴展](#extensions-with-a-generic-where-clause) 中有討論。
這是上面 [泛型類型](#generic-types) 中 `Stack` 類型的擴展,它遵循了 SuffixableContainer 協議:
```swift
extension Stack: SuffixableContainer {
func suffix(_ size: Int) -> Stack {
var result = Stack()
for index in (count-size)..<count {
result.append(self[index])
}
return result
}
// 推斷 suffix 結果是Stack。
}
var stackOfInts = Stack<Int>()
stackOfInts.append(10)
stackOfInts.append(20)
stackOfInts.append(30)
let suffix = stackOfInts.suffix(2)
// suffix 包含 20 和 30
```
在上面的例子中,`Suffix` 是 `Stack` 的關聯類型,也是 `Stack` ,所以 `Stack` 的后綴運算返回另一個 `Stack` 。另外,遵循 `SuffixableContainer` 的類型可以擁有一個與它自己不同的 `Suffix` 類型——也就是說后綴運算可以返回不同的類型。比如說,這里有一個非泛型 `IntStack` 類型的擴展,它遵循了 `SuffixableContainer` 協議,使用 `Stack<Int>` 作為它的后綴類型而不是 `IntStack`:
```swift
extension IntStack: SuffixableContainer {
func suffix(_ size: Int) -> Stack<Int> {
var result = Stack<Int>()
for index in (count-size)..<count {
result.append(self[index])
}
return result
}
// 推斷 suffix 結果是 Stack<Int>。
}
```
## 泛型 Where 語句 {#where-clauses}
[類型約束](#type-constraints) 讓你能夠為泛型函數、下標、類型的類型參數定義一些強制要求。
對關聯類型添加約束通常是非常有用的。你可以通過定義一個泛型 `where` 子句來實現。通過泛型 `where` 子句讓關聯類型遵從某個特定的協議,以及某個特定的類型參數和關聯類型必須類型相同。你可以通過將 `where` 關鍵字緊跟在類型參數列表后面來定義 `where` 子句,`where` 子句后跟一個或者多個針對關聯類型的約束,以及一個或多個類型參數和關聯類型間的相等關系。你可以在函數體或者類型的大括號之前添加 `where` 子句。
下面的例子定義了一個名為 `allItemsMatch` 的泛型函數,用來檢查兩個 `Container` 實例是否包含相同順序的相同元素。如果所有的元素能夠匹配,那么返回 `true`,否則返回 `false`。
被檢查的兩個 `Container` 可以不是相同類型的容器(雖然它們可以相同),但它們必須擁有相同類型的元素。這個要求通過一個類型約束以及一個 `where` 子句來表示:
```swift
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
}
}
// 所有元素都匹配,返回 true
return true
}
```
這個函數接受 `someContainer` 和 `anotherContainer` 兩個參數。參數 `someContainer` 的類型為 `C1`,參數 `anotherContainer` 的類型為 `C2`。`C1` 和 `C2` 是容器的兩個占位類型參數,函數被調用時才能確定它們的具體類型。
這個函數的類型參數列表還定義了對兩個類型參數的要求:
+ `C1` 必須符合 `Container` 協議(寫作 `C1: Container`)。
+ `C2` 必須符合 `Container` 協議(寫作 `C2: Container`)。
+ `C1` 的 `Item` 必須和 `C2` 的 `Item` 類型相同(寫作 `C1.Item == C2.Item`)。
+ `C1` 的 `Item` 必須符合 `Equatable` 協議(寫作 `C1.Item: Equatable`)。
前兩個要求定義在函數的類型形式參數列表里,后兩個要求定義在了函數的泛型 `where` 分句中。
這些要求意味著:
+ `someContainer` 是一個 `C1` 類型的容器。
+ `anotherContainer` 是一個 `C2` 類型的容器。
+ `someContainer` 和 `anotherContainer` 包含相同類型的元素。
+ `someContainer` 中的元素可以通過不等于操作符(!=)來檢查它們是否相同。
第三個和第四個要求結合起來意味著 `anotherContainer` 中的元素也可以通過 `!=` 操作符來比較,因為它們和 `someContainer` 中的元素類型相同。
這些要求讓 `allItemsMatch(_:_:)` 函數能夠比較兩個容器,即使它們的容器類型不同。
`allItemsMatch(_:_:)` 函數首先檢查兩個容器元素個數是否相同,如果元素個數不同,那么一定不匹配,函數就會返回 `false`。
進行這項檢查之后,通過 `for-in` 循環和半閉區間操作符(`..<`)來迭代每個元素,檢查 `someContainer` 中的元素是否不等于 `anotherContainer` 中的對應元素。如果兩個元素不相等,那么兩個容器不匹配,函數返回 false。
如果循環體結束后未發現任何不匹配的情況,表明兩個容器匹配,函數返回 `true`。
下面是 `allItemsMatch(_:_:)` 函數的示例:
```swift
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
var arrayOfStrings = ["uno", "dos", "tres"]
if allItemsMatch(stackOfStrings, arrayOfStrings) {
print("All items match.")
} else {
print("Not all items match.")
}
// 打印“All items match.”
```
上面的例子創建 `Stack` 實例來存儲 `String` 值,然后將三個字符串壓棧。這個例子還通過數組字面量創建了一個 `Array` 實例,數組中包含同棧中一樣的三個字符串。即使棧和數組是不同的類型,但它們都遵從 `Container` 協議,而且它們都包含相同類型的值。因此你可以用這兩個容器作為參數來調用 `allItemsMatch(_:_:)` 函數。在上面的例子中,`allItemsMatch(_:_:)` 函數正確地顯示了這兩個容器中的所有元素都是相互匹配的。
## 具有泛型 Where 子句的擴展 {#extensions-with-a-generic-where-clause}
你也可以使用泛型 `where` 子句作為擴展的一部分。基于以前的例子,下面的示例擴展了泛型 `Stack` 結構體,添加一個 `isTop(_:)` 方法。
```swift
extension Stack where Element: Equatable {
func isTop(_ item: Element) -> Bool {
guard let topItem = items.last else {
return false
}
return topItem == item
}
}
```
這個新的 `isTop(_:)` 方法首先檢查這個棧是不是空的,然后比較給定的元素與棧頂部的元素。如果你嘗試不用泛型 `where` 子句,會有一個問題:在 `isTop(_:)` 里面使用了 `==` 運算符,但是 `Stack` 的定義沒有要求它的元素是符合 `Equatable` 協議的,所以使用 `==` 運算符導致編譯時錯誤。使用泛型 `where` 子句可以為擴展添加新的條件,因此只有當棧中的元素符合 `Equatable` 協議時,擴展才會添加 `isTop(_:)` 方法。
以下是 `isTop(_:)` 方法的調用方式:
```swift
if stackOfStrings.isTop("tres") {
print("Top element is tres.")
} else {
print("Top element is something else.")
}
// 打印“Top element is tres.”
```
如果嘗試在其元素不符合 `Equatable` 協議的棧上調用 `isTop(_:)` 方法,則會收到編譯時錯誤。
```swift
struct NotEquatable { }
var notEquatableStack = Stack<NotEquatable>()
let notEquatableValue = NotEquatable()
notEquatableStack.push(notEquatableValue)
notEquatableStack.isTop(notEquatableValue) // 報錯
```
你可以使用泛型 `where` 子句去擴展一個協議。基于以前的示例,下面的示例擴展了 `Container` 協議,添加一個 `startsWith(_:)` 方法。
```swift
extension Container where Item: Equatable {
func startsWith(_ item: Item) -> Bool {
return count >= 1 && self[0] == item
}
}
```
這個 `startsWith(_:)` 方法首先確保容器至少有一個元素,然后檢查容器中的第一個元素是否與給定的元素相等。任何符合 `Container` 協議的類型都可以使用這個新的 `startsWith(_:)` 方法,包括上面使用的棧和數組,只要容器的元素是符合 `Equatable` 協議的。
```swift
if [9, 9, 9].startsWith(42) {
print("Starts with 42.")
} else {
print("Starts with something else.")
}
// 打印“Starts with something else.”
```
上述示例中的泛型 `where` 子句要求 `Item` 遵循協議,但也可以編寫一個泛型 `where` 子句去要求 `Item` 為特定類型。例如:
```swift
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”
```
此示例將一個 `average()` 方法添加到 `Item` 類型為 `Double` 的容器中。此方法遍歷容器中的元素將其累加,并除以容器的數量計算平均值。它將數量從 `Int` 轉換為 `Double` 確保能夠進行浮點除法。
就像可以在其他地方寫泛型 `where` 子句一樣,你可以在一個泛型 `where` 子句中包含多個條件作為擴展的一部分。用逗號分隔列表中的每個條件。
## 包含上下文關系的 where 分句 {#contextual-where-clauses}
當你使用泛型時,可以為沒有獨立類型約束的聲明添加 `where` 分句。例如,你可以使用 `where` 分句為泛型添加下標,或為擴展方法添加泛型約束。`Container` 結構體是個泛型,下面的例子通過 `where` 分句讓新的方法聲明其調用所需要滿足的類型約束。
```swift
extension Container {
func average() -> Double where Item == Int {
var sum = 0.0
for index in 0..<count {
sum += Double(self[index])
}
return sum / Double(count)
}
func endsWith(_ item: Item) -> Bool where Item: Equatable {
return count >= 1 && self[count-1] == item
}
}
let numbers = [1260, 1200, 98, 37]
print(numbers.average())
// 輸出 "648.75"
print(numbers.endsWith(37))
// 輸出 "true"
```
例子中,當 `Item` 是整型時為 `Container` 添加 `average()` 方法,當 `Item` 遵循 `Equatable` 時添加 `endsWith(_:)` 方法。兩個方法都通過 `where` 分句對 `Container` 中定義的泛型 `Item` 進行了約束。
如果不使用包含上下文關系的 `where` 分句,需要寫兩個擴展,并為每個擴展分別加上 `where` 分句。下面的例子和上面的具有相同效果。
```swift
extension Container where Item == Int {
func average() -> Double {
var sum = 0.0
for index in 0..<count {
sum += Double(self[index])
}
return sum / Double(count)
}
}
extension Container where Item: Equatable {
func endsWith(_ item: Item) -> Bool {
return count >= 1 && self[count-1] == item
}
}
```
在包含上下文關系的 `where` 分句的例子中,由于每個方法的 `where` 分句各自聲明了需要滿足的條件,因此 `average()` 和 `endsWith(_:)` 的實現能放在同一個擴展里。而將 `where` 分句放在擴展進行聲明也能起到同樣的效果,但每一個擴展只能有一個必備條件。
## 具有泛型 Where 子句的關聯類型 {#associated-types-with-a-generic-where-clause}
你可以在關聯類型后面加上具有泛型 `where` 的子句。例如,建立一個包含迭代器(`Iterator`)的容器,就像是標準庫中使用的 `Sequence` 協議那樣。你應該這么寫:
```swift
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
}
```
迭代器(`Iterator`)的泛型 `where` 子句要求:無論迭代器是什么類型,迭代器中的元素類型,必須和容器項目的類型保持一致。`makeIterator()` 則提供了容器的迭代器的訪問接口。
一個協議繼承了另一個協議,你通過在協議聲明的時候,包含泛型 `where` 子句,來添加了一個約束到被繼承協議的關聯類型。例如,下面的代碼聲明了一個 `ComparableContainer` 協議,它要求所有的 `Item` 必須是 `Comparable` 的。
```swift
protocol ComparableContainer: Container where Item: Comparable { }
```
## 泛型下標 {#generic-subscripts}
下標可以是泛型,它們能夠包含泛型 `where` 子句。你可以在 `subscript` 后用尖括號來寫占位符類型,你還可以在下標代碼塊花括號前寫 `where` 子句。例如:
```swift
extension Container {
subscript<Indices: Sequence>(indices: Indices) -> [Item]
where Indices.Iterator.Element == Int {
var result = [Item]()
for index in indices {
result.append(self[index])
}
return result
}
}
```
這個 `Container` 協議的擴展添加了一個下標方法,接收一個索引的集合,返回每一個索引所在的值的數組。這個泛型下標的約束如下:
+ 在尖括號中的泛型參數 `Indices`,必須是符合標準庫中的 `Sequence` 協議的類型。
+ 下標使用的單一的參數,`indices`,必須是 `Indices` 的實例。
+ 泛型 `where` 子句要求 `Sequence(Indices)`的迭代器,其所有的元素都是 `Int` 類型。這樣就能確保在序列(`Sequence`)中的索引和容器(`Container`)里面的索引類型是一致的。
綜合一下,這些約束意味著,傳入到 `indices` 下標,是一個整型的序列。
- 1.關于 Swift
- 2.Swift 初見
- 2-1基礎部分
- 2-2基本運算符
- 2-3字符串和字符
- 2-4集合類型
- 2-5控制流
- 2-6函數
- 2-7閉包
- 2-8枚舉
- 2-9類和結構體
- 2-10屬性
- 2-11方法
- 2-12下標
- 2-13繼承
- 2-14構造過程
- 2-15析構過程
- 2-16可選鏈
- 2-17錯誤處理
- 2-18類型轉換
- 2-19嵌套類型
- 2-20擴展
- 2-21協議
- 2-22泛型
- 2-23不透明類型
- 2-24自動引用計數
- 2-25內存安全
- 2-26訪問控制
- 2-27高級運算符
- 3-1關于語言參考
- 3-2詞法結構
- 3-3類型
- 3-4表達式
- 3-5語句
- 3-6聲明
- 3-7特性
- 3-8模式
- 3-9泛型參數
- 4語法總結