一篇帶給你Swift 進階: 泛型
1. 前言
泛型代碼讓你能根據(jù)你所定義的要求寫出可以用于任何類型的靈活的、可復用的函數(shù)。你可以編寫出可復用、意圖表達清晰、抽象的代碼。
泛型是 Swift 最強大的特性之一,很多 Swift 標準庫是基于泛型代碼構(gòu)建的。實際上,甚至你都沒有意識到在語言指南中一直在使用泛型。例如,Swift 的 Array 和 Dictionary 類型都是泛型集合。
你可以創(chuàng)建一個容納 Int 值的數(shù)組,或者容納String 值的數(shù)組,甚至容納任何 Swift 可以創(chuàng)建的其他類型的數(shù)組。同樣,你可以創(chuàng)建一個存儲任何指定類型值的字典,而且類型沒有限制。
2. 泛型解決的問題
下面的 swapTwoInts(_:_:) 是一個標準的非泛型函數(shù),用于交換兩個 Int 值:
- func swapTwoInts(_ a: inout Int, _ b: inout Int) {
- let temporaryA = a
- a = b
- b = temporaryA
- }
如輸入輸出形式參數(shù)中描述的一樣,這個函數(shù)用輸入輸出形式參數(shù)來交換 a 和 b 的值。
swapTwoInts(_:_:) 函數(shù)把 b 原本的值給 a ,把 a 原本的值給 b 。你可以調(diào)用這個函數(shù)來交換兩個 Int 變量的值。
- var someInt = 3
- var anotherInt = 107
- swapTwoInts(&someInt, &anotherInt)
- print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
- // Prints "someInt is now 107, and anotherInt is now 3"
swapTwoInts(_:_:) 函數(shù)很實用,但是它只能用于 Int 值。如果你想交換兩個 String 值,或者兩個 Double 值,你只能再寫更多的函數(shù),比如下面的 swapTwoStrings(_:_:) 和swapTwoDoubles(_:_:) 函數(shù):
- 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
- }
你可能已經(jīng)注意到了, swapTwoInts(_:_:) 、 swapTwoStrings(_:_:) 、 swapTwoDoubles(_:_:) 函數(shù)體是一樣的。唯一的區(qū)別是它們接收值類型不同( Int 、 String 和 Double )。
寫一個可以交換任意類型值的函數(shù)會更實用、更靈活。泛型代碼讓你能寫出這樣的函數(shù)。(下文中定義了這些函數(shù)的泛型版本。)
三個函數(shù)中, a 和 b 被定義為了相同的類型,這一點很重要。如果 a 和 b 類型不一樣,不能交換它們的值。Swift 是類型安全的語言,不允許(例如)一個 String 類型的變量和一個Double 類型的變量交換值。嘗試這樣做會引發(fā)一個編譯錯誤。
3. 泛型函數(shù)
泛型函數(shù)可以用于任何類型。這里是上面提到的 swapTwoInts(_:_:) 函數(shù)的泛型版本,叫做swapTwoValues(_:_:) :
- func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
- let temporaryA = a
- a = b
- b = temporaryA
- }
swapTwoValues(_:_:) 和 swapTwoInts(_:_:) 函數(shù)體是一樣的。但是, swapTwoValues(_:_:) 和swapTwoInts(_:_:) 的第一行有點不一樣。下面是首行的對比:
- func swapTwoInts(_ a: inout Int, _ b: inout Int)
- func swapTwoValues<T>(_ a: inout T, _ b: inout T)
泛型版本的函數(shù)用了一個占位符類型名(這里叫做 T ),而不是一個實際的類型名(比如 Int 、String 或 Double )。占位符類型名沒有聲明 T 必須是什么樣的,但是它確實說了 a 和 b 必須都是同一個類型 T ,或者說都是 T 所表示的類型。替代 T 實際使用的類型將在每次調(diào)用swapTwoValues(_:_:) 函數(shù)時決定。
其他的區(qū)別是泛型函數(shù)名( swapTwoValues(_:_:) )后面有包在尖括號(
現(xiàn)在,可以用調(diào)用 swapTwoInts 的方式來調(diào)用 swapTwoValues(_:_:) 函數(shù),除此之外,可以給函數(shù)傳遞兩個任意類型的值,只要兩個實參的類型一致即可。每次調(diào)用 swapTwoValues(_:_:) ,用于T 的類型會根據(jù)傳入函數(shù)的值類型自動推斷。
在下面的兩個例子中, T分別被推斷為 Int 和 String :
- var someInt = 3
- var anotherInt = 107
- swapTwoValues(&someInt, &anotherInt)
- // someInt is now 107, and anotherInt is now 3
- var someString = "hello"
- var anotherString = "world"
- swapTwoValues(&someString, &anotherString)
- // someString is now "world", and anotherString is now "hello"
上面定義的 swapTwoValues(_:_:) 函數(shù)受一個名為 swap 的泛型函數(shù)啟發(fā), swap 函數(shù)是 Swift 標準庫的一部分,可以用于你的應用中。如果你需要在你自己的代碼中用swapTwoValues(_:_:) 函數(shù)的功能,可以直接用 Swift 提供的 swap(_:_:) 函數(shù),不需要自己實現(xiàn)。
4. 類型形式參數(shù)
上面的 swapTwoValues(_:_:) 中,占位符類型 T 就是一個類型形式參數(shù)的例子。類型形式參數(shù)指定并且命名一個占位符類型,緊挨著寫在函數(shù)名后面的一對尖括號里(比如
一旦你指定了一個類型形式參數(shù),你就可以用它定義一個函數(shù)形式參數(shù)(比如swapTwoValues(_:_:) 函數(shù)中的形式參數(shù) a 和 b )的類型,或者用它做函數(shù)返回值類型,或者做函數(shù)體中類型標注。在不同情況下,用調(diào)用函數(shù)時的實際類型來替換類型形式參數(shù)。(上面的swapTwoValues(_:_:) 例子中,第一次調(diào)用函數(shù)的時候用 Int 替換了 T ,第二次調(diào)用是用 String替換的。)
你可以通過在尖括號里寫多個用逗號隔開的類型形式參數(shù)名,來提供更多類型形式參數(shù)。
5. 命名類型形式參數(shù)
大多數(shù)情況下,類型形式參數(shù)的名字要有描述性,比如 Dictionary
類型形式參數(shù)永遠用大寫開頭的駝峰命名法(比如T和MyTypeParameter)命名,以指明它們是一個類型的占位符,不是一個值。
6. 泛型類型
除了泛型函數(shù),Swift允許你定義自己的泛型類型。它們是可以用于任意類型的自定義類、結(jié)構(gòu)體、枚舉,和 Array 、 Dictionary 方式類似。
本章將向你展示如何寫出一個叫做 Stack 的泛型集合類型。棧是值的有序集合,和數(shù)組類似,但是比 Swift 的 Array 類型有更嚴格的操作限制。數(shù)組允許在其中任何位置插入和移除元素。但是,棧的新元素只能添加到集合的末尾(這就是所謂的壓棧)。同樣,棧只允許從集合的末尾移除元素(這就是所謂的出棧)。
UINavigationController類在它的導航層級關(guān)系中管理視圖控制器就是用的棧的思想。你可以調(diào)用UINavigationController類的pushViewController(_:animated:) 方法添加(或者說push)一個視圖控制器到導航棧里,用popViewControllerAnimated(_:) 方法從導航棧移除(或者說pop)一個視圖控制器。當你需要用嚴格的”后進,先出”方式管理一個集合時,棧是一個很有用的集合模型。
下面的圖示展示了壓棧和出棧的行為:

- 現(xiàn)在棧里有三個值;
- 第四個值壓到棧頂;
- 棧里現(xiàn)在有四個值,最近添加的那個在頂部;
- 棧中頂部的元素被移除,或者說叫”出棧”;
- 移除一個元素之后,棧里又有三個元素了。
這里是如何寫一個非泛型版本的棧,這種情況是一個 Int 值的棧:
- struct IntStack {
- var items = [Int]()
- mutating func push(_ item: Int) {
- items.append(item)
- }
- mutating func pop() -> Int {
- return items.removeLast()
- }
- }
這個結(jié)構(gòu)體用了一個叫做 items 的 Array 屬性去存儲棧中的值。 Stack 提供兩個方法, push 和pop ,用于添加和移除棧中的值。這兩個方法被標記為 mutating ,是因為他們需要修改(或者說改變)結(jié)構(gòu)體的 items 數(shù)組。
上面展示的 IntStack 類型只能用于 Int 值。但是定義一個泛型 Stack 會更實用,這樣可以管理任何類型值的棧。
這里有一個相同代碼的泛型版本:
- struct Stack<Element> {
- var items = [Element]()
- mutating func push(_ item: Element) {
- items.append(item)
- }
- mutating func pop() -> Element {
- return items.removeLast()
- }
- }
注意,這個泛型的 Stack 和非泛型版本的本質(zhì)上是一樣的,只是用一個叫做 Element 的類型形式參數(shù)代替了實際的 Int 類型。這個類型形式參數(shù)寫在一對尖括號(
Element 為稍后提供的”某類型 Element “定義了一個占位符名稱。這個未來的類型可以在結(jié)構(gòu)體定義內(nèi)部任何位置以” Element “引用。在這個例子中,有三個地方將 Element 作為一個占位符使用:
- 創(chuàng)建一個名為 items 的屬性,用一個 Element 類型值的空數(shù)組初始化這個屬性;
- 指定 push(_:) 方法有一個叫做 item 的形式參數(shù),其必須是 Element 類型;
- 指定 pop() 方法的返回值是一個 Element 類型的值。
因為它是泛型,因此能以 Array 和 Dictionary 相似的方式,用 Stack 創(chuàng)建一個Swift 中有效的任意類型的棧。
通過在尖括號中寫出存儲在棧里的類型,來創(chuàng)建一個新的 Stack 實例。例如,創(chuàng)建一個新的字符串棧,可以寫 Stack
- var stackOfStrings = Stack<String>()
- stackOfStrings.push("uno")
- stackOfStrings.push("dos")
- stackOfStrings.push("tres")
- stackOfStrings.push("cuatro")
- // the stack now contains 4 strings
這是往棧里壓入四個值之后, stackOfStrings 的圖示:

從棧中移除并返回頂部的值,"cuatro" :
- let fromTheTop = stackOfStrings.pop()
- // fromTheTop is equal to "cuatro", and the stack now contains 3 strings
這是棧頂部的值出棧后的棧圖示:

7. 擴展一個泛型類型
當你擴展一個泛型類型時,不需要在擴展的定義中提供類型形式參數(shù)列表。原始類型定義的類型形式參數(shù)列表在擴展體里仍然有效,并且原始類型形式參數(shù)列表名稱也用于擴展類型形式參數(shù)。
下面的例子擴展了泛型 Stack 類型,向其中添加一個叫做 topItem 的只讀計算屬性,不需要從棧里移除就能返回頂部的元素:
- extension Stack {
- var topItem: Element? {
- return items.isEmpty ? nil : items[items.count - 1]
- }
- }
topItem 屬性返回一個 Element 類型的可選值。如果棧是空的, topItem 返回 nil ;如果棧非空, topItem 返回 items 數(shù)組的最后一個元素。
注意,這個擴展沒有定義類型形式參數(shù)列表。相反,擴展中用 Stack已有的類型形式參數(shù)名稱,Element ,來指明計算屬性 topItem 的可選項類型。
現(xiàn)在,不用移除元素,就可以用任何 Stack 實例的 topItem 計算屬性來訪問和查詢它頂部的元素:
- if let topItem = stackOfStrings.topItem {
- print("The top item on the stack is \(topItem).")
- }
- // Prints "The top item on the stack is tres."
8. 類型約束
swapTwoValues(_:_:) 函數(shù)和 Stack 類型可以用于任意類型。但是,有時在用于泛型函數(shù)的類型和泛型類型上,強制其遵循特定的類型約束很有用。類型約束指出一個類型形式參數(shù)必須繼承自特定類,或者遵循一個特定的協(xié)議、組合協(xié)議。
例如,Swift 的 Dictionary 類型在可以用于字典中鍵的類型上設(shè)置了一個限制。如字典中描述的一樣,字典鍵的類型必須是是可哈希的。也就是說,它必須提供一種使其可以唯一表示的方法。Dictionary 需要它的鍵是可哈希的,以便它可以檢查字典中是否包含一個特定鍵的值。沒有了這個要求, Dictionary 不能區(qū)分該插入還是替換一個指定鍵的值,也不能在字典中查找已經(jīng)給定的鍵的值。
這個要求通過 Dictionary 鍵類型上的類型約束實現(xiàn),它指明了鍵類型必須遵循 Swift 標準庫中定義的 Hashable 協(xié)議。所有 Swift 基本類型(比如 String 、 Int 、 Double 和 Bool )默認都是可哈希的。
創(chuàng)建自定義泛型類型時,你可以定義你自己的類型約束,這些約束可以提供強大的泛型編程能力。像 Hashable 這樣的抽象概念,根據(jù)概念上的特征,而不是確切的類型來表征類型。
▐ 8.1 類型約束語法
在一個類型形式參數(shù)名稱后面放置一個類或者協(xié)議作為形式參數(shù)列表的一部分,并用冒號隔開,以寫出一個類型約束。下面展示了一個泛型函數(shù)類型約束的基本語法(和泛型類型的語法相同):
- func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
- // function body goes here
- }
上面的假想函數(shù)有兩個形式參數(shù)。第一個類型形式參數(shù), T ,有一個類型約束要求 T 是SomeClass 的子類。第二個類型形式參數(shù), U ,有一個類型約束要求 U 遵循 SomeProtocol 協(xié)議。
▐ 8.2 類型約束的應用
這是一個叫做 findIndex(ofString:in:) 的非泛型函數(shù),在給定的 String 值數(shù)組中查找給定的String 值。 findIndex(ofString:in:) 函數(shù)返回一個可選的 Int 值,如果找到了給定字符串,它會返回數(shù)組中第一個匹配的字符串的索引值,如果找不到給定字符串就返回 nil:
- 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:) 函數(shù)可以用于字符串數(shù)組中查找字符串值:
- let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]
- if let foundIndex = findIndex(ofString: "llama", in: strings) {
- print("The index of llama is \(foundIndex)")
- }
- // Prints "The index of llama is 2"
在數(shù)組中查找值的索引的原理只能用于字符串。但是,通過某種 T 類型的值代替所有用到的字符串,你可以用泛型函數(shù)寫一個相同的功能。
這里寫出了一個叫做 findIndex(of:in:) 的函數(shù),可能是你期望的 findIndex(ofString:in:) 函數(shù)的一個泛型版本。注意,函數(shù)的返回值仍然是 Int? ,因為函數(shù)返回一個可選的索引數(shù)字,而不是數(shù)組里的一個可選的值。這個函數(shù)沒有編譯,例子后面會解釋原因:
- func findIndex<T>(of valueToFind: T, in array:[T]) -> Int? {
- for (index, value) in array.enumerated() {
- if value == valueToFind {
- return index
- }
- }
- return nil
- }
這個函數(shù)沒有像上面寫的那樣編譯。問題在于相等檢查,”if value == valueToFind “。Swift 中的類型不是每種都能用相等操作符( == )來比較的。如果你創(chuàng)建自己的類或者結(jié)構(gòu)體去描述一個復雜的數(shù)據(jù)模型,比如說,對于那個類或結(jié)構(gòu)體來說,”相等”的意義不是 Swift 能替你猜出來的。因此,不能保證這份代碼可以用于所有 T 可以表示的類型,當你嘗試編譯這份代碼時會提示一個相應的錯誤。
并非無路可走,總之,Swift 標準庫中定義了一個叫做 Equatable 的協(xié)議,要求遵循其協(xié)議的類型要實現(xiàn)相等操作符( == )和不等操作符( != ),用于比較該類型的任意兩個值。所有 Swift 標準庫中的類型自動支持 Equatable 協(xié)議。
任何 Equatable 的類型都能安全地用于 findIndex(of:in:) 函數(shù),因為可以保證那些類型支持相等操作符。為了表達這個事實,當你定義函數(shù)時將 Equatable 類型約束作為類型形式參數(shù)定義的一部分書寫:
- 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:) 的類型形式參數(shù)寫作 T: Equatable ,表示”任何遵循 Equatable 協(xié)議的類型 T“。
findIndex(of:in:) 函數(shù)現(xiàn)在可以成功編譯,并且可以用于任何 Equatable 的類型,比如 Double或者 String :
- let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25])
- // doubleIndex is an optional Int with no value, because 9.3 is not in the array
- let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"])
- // stringIndex is an optional Int containing a value of 2
9. 關(guān)聯(lián)類型
定義一個協(xié)議時,有時在協(xié)議定義里聲明一個或多個關(guān)聯(lián)類型是很有用的。關(guān)聯(lián)類型給協(xié)議中用到的類型一個占位符名稱。直到采納協(xié)議時,才指定用于該關(guān)聯(lián)類型的實際類型。關(guān)聯(lián)類型通過associatedtype 關(guān)鍵字指定。
▐ 9.1 關(guān)聯(lián)類型的應用
這里是一個叫做 Container 的示例協(xié)議,聲明了一個叫做 ItemType 的關(guān)聯(lián)類型:
- protocol Container {
- associatedtype ItemType
- mutating func append(_ item: ItemType)
- var count: Int { get }
- subscript(i: Int) -> ItemType { get }
- }
Container 協(xié)議定義了三個所有容器必須提供的功能:
- 必須能夠通過 append(_:) 方法向容器中添加新元素;
- 必須能夠通過一個返回 Int 值的 count 屬性獲取容器中的元素數(shù)量;
- 必須能夠通過 Int 索引值的下標取出容器中每個元素。
這個協(xié)議沒有指定元素如何儲存在容器中,也沒指定允許存入容器的元素類型。協(xié)議僅僅指定了想成為一個 Container 的類型,必須提供的三種功能。遵循該協(xié)議的類型可以提供其他功能,只要滿足這三個要求即可。
任何遵循 Container 協(xié)議的類型必須能指定其存儲值的類型。尤其是它必須保證只有正確類型的元素才能添加到容器中,而且該類型下標返回的元素類型必須是正確的。
為了定義這些要求, Container 協(xié)議需要一種在不知道容器具體類型的情況下,引用該容器將存儲的元素類型的方法。 Container 協(xié)議需要指定所有傳給 append(_:) 方法的值必須和容器里元素的值類型是一樣的,而且容器下標返回的值也是和容器里元素的值類型相同。
為了實現(xiàn)這些要求, Container 協(xié)議聲明了一個叫做 ItemType 的關(guān)聯(lián)類型,寫作 associatedtype ItemType 。協(xié)議沒有定義 ItemType 是什么類型,這個信息留給遵循協(xié)議的類型去提供。但是,ItemType 這個別名,提供了一種引用 Container 中元素類型的方式,定義了一種用于 Container方法和下標的類型,確保了任何 Container 期待的行為都得到滿足。
這是前面非泛型版本的 IntStack ,使其遵循 Container 協(xié)議:
- struct IntStack: Container {
- // original IntStack implementation
- var items = [Int]()
- mutating func push(_ item: Int) {
- items.append(item)
- }
- mutating func pop() -> Int {
- return items.removeLast()
- }
- // conformance to the Container protocol
- typealias ItemType = Int
- mutating func append(_ item: Int) {
- self.push(item)
- }
- var count: Int {
- return items.count
- }
- subscript(i: Int) -> Int {
- return items[i]
- }
- }
IntStack 實現(xiàn)了 Container 協(xié)議所有的要求,為滿足這些要求,封裝了 IntStack 里現(xiàn)有的方法。
此外, IntStack 為了實現(xiàn) Container 協(xié)議,指定了適用于 ItemType 的類型是 Int 類型。typealias ItemType = Int 把 ItemType 抽象類型轉(zhuǎn)換為了具體的 Int 類型。
感謝 Swift 的類型推斷功能,你不用真的在 IntStack 定義中聲明一個具體的 Int 類型 ItemType。因為 IntStack 遵循 Container 協(xié)議的所有要求,通過簡單查看 append(_:) 方法的 item 形式參數(shù)和下標的返回類型,Swift 可以推斷出合適的 ItemType 。如果你真的從上面的代碼中刪除typealias ItemType = Int ,一切都會正常運行,因為 ItemType 該用什么類型是非常明確的。
你也可以做一個遵循 Container 協(xié)議的泛型 Stack 類型:
- struct Stack<Element>: Container {
- // original Stack<Element> implementation
- var items = [Element]()
- mutating func push(_ item: Element) {
- items.append(item)
- }
- mutating func pop() -> Element {
- return items.removeLast()
- }
- // conformance to the Container protocol
- mutating func append(_ item: Element) {
- self.push(item)
- }
- var count: Int {
- return items.count
- }
- subscript(i: Int) -> Element {
- return items[i]
- }
- }
這次,類型形式參數(shù) Element 用于 append(_:) 方法的 item 形式參數(shù)和下標的返回類型。因此,對于這個容器,Swift 可以推斷出 Element 是適用于 ItemType 的類型。
▐ 9.2 給關(guān)聯(lián)類型添加約束
你可以在協(xié)議里給關(guān)聯(lián)類型添加約束來要求遵循的類型滿足約束。比如說,下面的代碼定義了一個版本的 Container ,它要求容器中的元素都是可判等的。
- protocol Container {
- associatedtype Item: Equatable
- mutating func append(_ item: Item)
- var count: Int { get }
- subscript(i: Int) -> Item { get }
- }
要遵循這個版本的 Container ,容器的 Item 必須遵循 Equatable 協(xié)議。
▐ 9.3 在關(guān)聯(lián)類型約束里使用協(xié)議
協(xié)議可以作為它自身的要求出現(xiàn)。比如說,這里有一個協(xié)議細化了 Container 協(xié)議,添加了一個 suffix(_:) 方法。 suffix(_:) 方法返回容器中從后往前給定數(shù)量的元素,把它們存儲在一個 Suffix 類型的實例里。
- protocol SuffixableContainer: Container {
- associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
- func suffix(_ size: Int) -> Suffix
- }
在這個協(xié)議里, Suffix 是一個關(guān)聯(lián)類型,就像上邊例子中 Container 的 Item 類型一樣。Suffix 擁有兩個約束:它必須遵循 SuffixableContainer 協(xié)議(就是當前定義的協(xié)議),以及它的 Item 類型必須是和容器里的 Item 類型相同。Item 的約束是一個 where 分句,它在下面帶有泛型 Where 分句的擴展中有討論。
這里有一個來自閉包的循環(huán)強引用的 Stack 類型的擴展,它添加了對 SuffixableContainer協(xié)議的遵循:
- extension Stack: SuffixableContainer {
- func suffix(_ size: Int) -> Stack {
- var result = Stack()
- for index in (count-size)..<count {
- result.append(self[index])
- }
- return result
- }
- // Inferred that Suffix is Stack.
- }
- var stackOfInts = Stack<Int>()
- stackOfInts.append(10)
- stackOfInts.append(20)
- stackOfInts.append(30)
- let suffix = stackOfInts.suffix(2)
- // suffix contains 20 and 30
在上面的例子中, Suffix 是 Stack 的關(guān)聯(lián)類型,也就是 Stack ,所以 Stack 的后綴運算返回另一個 Stack 。另外,遵循 SuffixableContainer 的類型可以擁有一個與它自己不同的Suffix 類型——也就是說后綴運算可以返回不同的類型。比如說,這里有一個非泛型IntStack 類型的擴展,它添加了 SuffixableContainer 遵循,使用 Stack
- 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
- }
- // Inferred that Suffix is Stack<Int>.
- }
▐ 9.4 擴展現(xiàn)有類型來指定關(guān)聯(lián)類型
你可以擴展一個現(xiàn)有類型使其遵循一個協(xié)議,如在擴展里添加協(xié)議遵循描述的一樣。這包括一個帶關(guān)聯(lián)類型的協(xié)議。
Swift 的 Array 類型已經(jīng)提供了 append(_:) 方法、 count 屬性、用 Int 索引取出其元素的下標。這三個功能滿足了 Container 協(xié)議的要求。這意味著你可以通過簡單地聲明 Array 采納協(xié)議,擴展 Array 使其遵循 Container 協(xié)議。通過一個空的擴展實現(xiàn),如使用擴展聲明采納協(xié)議:
- extension Array: Container {}
數(shù)組已有的 append(_:) 方法和下標使得 Swift 能為 ItemType 推斷出合適的類型,就像上面的泛型 Stack 類型一樣。定義這個擴展之后,你可以把任何 Array 當做一個 Container 使用。
10. 泛型Where分句
如類型約束中描述的一樣,類型約束允許你在泛型函數(shù)或泛型類型相關(guān)的類型形式參數(shù)上定義要求。
類型約束在為關(guān)聯(lián)類型定義要求時也很有用。通過定義一個泛型Where分句來實現(xiàn)。泛型 Where 分句讓你能夠要求一個關(guān)聯(lián)類型必須遵循指定的協(xié)議,或者指定的類型形式參數(shù)和關(guān)聯(lián)類型必須相同。泛型 Where 分句以 Where 關(guān)鍵字開頭,后接關(guān)聯(lián)類型的約束或類型和關(guān)聯(lián)類型一致的關(guān)系。泛型 Where 分句寫在一個類型或函數(shù)體的左半個大括號前面。
下面的例子定義了一個叫做 allItemsMatch 的泛型函數(shù),用來檢查兩個 Container 實例是否包含相同順序的相同元素。如果所有元素都匹配,函數(shù)返回布爾值 ture ,否則返回 false 。
被檢查的兩個容器不一定是相同類型的(盡管它們可以是),但是它們的元素類型必須相同。這個要求通過類型約束和泛型 Where 分句一起體現(xiàn):
- func allItemsMatch<C1: Container, C2: Container>
- (_ someContainer: C1, _ anotherContainer: C2) -> Bool
- where C1.ItemType == C2.ItemType, C1.ItemType: Equatable {
- // Check that both containers contain the same number of items.
- if someContainer.count != anotherContainer.count {
- return false
- }
- // Check each pair of items to see if they are equivalent.
- for i in 0..<someContainer.count {
- if someContainer[i] != anotherContainer[i] {
- return false
- }
- }
- // All items match, so return true.
- return true
- }
這個函數(shù)有兩個形式參數(shù),someContainer 和 anotherContainer 。 someContainer 形式參數(shù)是 C1類型, anotherContainer 形式參數(shù)是 C2 類型。 C1 和 C2 是兩個容器類型的類型形式參數(shù),它們的類型在調(diào)用函數(shù)時決定。
下面是函數(shù)的兩個類型形式參數(shù)上設(shè)置的要求:
- C1 必須遵循 Container 協(xié)議(寫作 C1: Container );
- C2 也必須遵循 Container 協(xié)議(寫作 C2: Container );
- C1 的 ItemType 必須和 C2 的 ItemType 相同(寫作 C1.ItemType == C2.ItemType );
- C1 的 ItemType 必須遵循 Equatable 協(xié)議(寫作 C1.ItemType: Equatable )。
前兩個要求定義在了函數(shù)的類型形式參數(shù)列表里,后兩個要求定義在了函數(shù)的泛型 Where 分句中。
這些要求意味著:
- someContainer 是一個 C1 類型的容器;
- anotherContainer 是一個 C2 類型的容器;
- someContainer 和 anotherContainer 中的元素類型相同;
- someContainer 中的元素可以通過不等操作符( != )檢查它們是否不一樣。
后兩個要求放到一起意味著, anotherContainer 中的元素也可以通過 != 操作符檢查,因為它們和 someContainer 中的元素類型完全相同。
這些要求使得 allItemsMatch(_:_:) 函數(shù)可以比較兩個容器,即使它們是不同類型的容器。
allItemsMatch(_:_:) 函數(shù)開始會先檢查兩個容器中的元素數(shù)量是否相同。如果它們的元素數(shù)量不同,它們不可能匹配,函數(shù)就會返回 false 。
檢查完數(shù)量之后,用一個 for-in 循環(huán)和半開區(qū)間操作符( ..< )遍歷 someContainer 中的所有元素。函數(shù)會檢查 someContainer 中的每個元素,是否和 anotherContainer 中對應的元素不相等。如果兩個元素不相等,則兩個容器不匹配,函數(shù)返回 false 。
如果循環(huán)完成都沒有出現(xiàn)不匹配的情況,兩個容器就是匹配的,則函數(shù)返回 true 。
這是 allItemsMatch(_:_:) 函數(shù)使用的示例:
- 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.")
- }
- // Prints "All items match."
上面的例子創(chuàng)建了一個 Stack 實例來存儲 String 值,壓到棧中三個字符串。還創(chuàng)建了一個 Array實例,用三個同樣字符串的字面量初始化該數(shù)組。雖然棧和數(shù)組的類型不一樣,但它們都遵循Container 協(xié)議,并且它們包含的值類型一樣。因此,你可以調(diào)用 allItemsMatch(_:_:) 函數(shù),用那兩個容器做函數(shù)的形式參數(shù)。上面的例子中, allItemsMatch(_:_:) 函數(shù)正確地報告了兩個容器中所有元素匹配。
11. 帶有泛型 Where 分句的擴展
你同時也可以使用泛型的 where 分句來作為擴展的一部分。下面的泛型 Stack 結(jié)構(gòu)體的擴展了先前的栗子,添加了一個 isTop(_:) 方法。
- extension Stack where Element: Equatable {
- func isTop(_ item: Element) -> Bool {
- guard let topItem = items.last else {
- return false
- }
- return topItem == item
- }
- }
這個新的 isTop(_:) 方法首先校驗棧不為空,然后對比給定的元素與棧頂元素。如果你嘗試不使用泛型 where 分句來做這個,你可能會遇到一個問題: isTop(_:) 的實現(xiàn)要使用 == 運算符,但Stack 的定義并不需要其元素可相等,所以使用 == 運算符會導致運行時錯誤。使用泛型 where分句則允許你給擴展添加一個新的要求,這樣擴展只會在棧內(nèi)元素可判等的時候才給棧添加isTop(_:) 方法。
這是用法:
- if stackOfStrings.isTop("tres") {
- print("Top element is tres.")
- } else {
- print("Top element is something else.")
- }
- // Prints "Top element is tres."
如果嘗試在元素不能判等的棧調(diào)用 isTop(_:) 方法,你就會出發(fā)運行時錯誤。
- struct NotEquatable { }
- var notEquatableStack = Stack<NotEquatable>()
- let notEquatableValue = NotEquatable()
- notEquatableStack.push(notEquatableValue)
- notEquatableStack.isTop(notEquatableValue) // Error
你可以使用泛型 where 分句來擴展到一個協(xié)議。下面的栗子把先前的 Container 協(xié)議擴展添加了一個 startsWith(_:) 方法。
- extension Container where Item: Equatable {
- func startsWith(_ item: Item) -> Bool {
- return count >= 1 && self[0] == item
- }
- }
startsWith(_:) 方法首先確保容器擁有至少一個元素,然后它檢查第一個元素是否與給定元素相同。這個新的 startsWith(_:) 方法可以應用到任何遵循 Container 協(xié)議的類型上,包括前面我們用的棧和數(shù)組,只要容器的元素可以判等。
- if [9, 9, 9].startsWith(42) {
- print("Starts with 42.")
- } else {
- print("Starts with something else.")
- }
- // Prints "Starts with something else."
上邊例子中的泛型 where 分句要求 Item 遵循協(xié)議,但你同樣可以寫一個泛型 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())
- // Prints "648.9"
這個栗子在 Item 是 Double 時給容器添加了 average() 方法。它遍歷容器中的元素來把它們相加,然后除以容器的總數(shù)來計算平均值。它顯式地把總數(shù)從 Int 轉(zhuǎn)為 Double 來允許浮點除法。
你可以在一個泛型 where 分句中包含多個要求來作為擴展的一部分,就如同你在其它地方寫的泛型 where 分句一樣。每一個需求用逗號分隔。
12. 上下文 Where 分句
當你已經(jīng)在范型類型上下文中時,你可以把范型 where 分句作為聲明的一部分,它自己沒有范型類型約束。比如說,你可以在范型類型的下標腳本或者范型類型擴展的方法中寫范型 where 分句。 Container 結(jié)構(gòu)體是范型,下邊例子中的 where 分句寫明了容器中新方法需要滿足什么要求才能可用。
- 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())
- // Prints "648.75"
- print(numbers.endsWith(37))
- // Prints "true"
這個例子在元素是整數(shù)時給 Container 添加了一個 average() 方法,它還在元素是可判等的情況下添加了 endsWith(_:) 方法。這兩個函數(shù)都包含了范型 where 分句,它給范型原本聲明在Container 中的形式參數(shù) Item 類型添加了類型限制。
如果你不想使用上下文 where 分句,你就需要寫兩個擴展,每一個都使用范型 where 分句。下面的例子和上面的例子有著相同的效果。
- 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 分句, average() 和 endsWith(_:) 都卸載了同一個擴展當中,因為每一個方法的范型 where 分句聲明了其生效所需要滿足的前提。把這些需求移動到擴展的范型 where 分句,可讓方法以相同的情況生效,但這就要求一個擴展對應一種需求了。
13. 關(guān)聯(lián)類型的泛型 Where 分句
你可以在關(guān)聯(lián)類型中包含一個泛型 where 分句。比如說,假定你想要做一個包含遍歷器的Container ,比如標準庫中 Sequence 協(xié)議那樣。那么你會這么寫:
- 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 分句要求遍歷器以相同的類型遍歷容器內(nèi)的所有元素,無論遍歷器是什么類型。 makeIterator() 函數(shù)提供了容器的遍歷器的訪問。
對于一個繼承自其他協(xié)議的協(xié)議來說,你可以通過在協(xié)議的聲明中包含泛型 where 分句來給繼承的協(xié)議中關(guān)聯(lián)類型添加限定。比如說,下面的代碼聲明了一個 ComparableContainer 協(xié)議,它要求 Item 遵循 Comparable :
- protocol ComparableContainer: Container where Item: Comparable { }
14. 泛型下標
下標可以是泛型,它們可以包含泛型 where 分句。你可以在 subscript 后用尖括號來寫類型占位符,你還可以在下標代碼塊花括號前寫泛型 where 分句。舉例來說:
- 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 協(xié)議的擴展添加了一個接收一系列索引并返回包含給定索引元素的數(shù)組。這個泛型下標有如下限定:
- 在尖括號中的泛型形式參數(shù) Indices 必須是遵循標準庫中 Sequence 協(xié)議的某類型;
- 下標接收單個形式參數(shù), indices ,它是一個 Indices 類型的實例;
- 泛型 where 分句要求序列的遍歷器必須遍歷 Int 類型的元素。這就保證了序列中的索引都是作為容器索引的相同類型。
合在一起,這些限定意味著傳入的 indices 形式參數(shù)是一個整數(shù)的序列。