鍍金池/ 教程/ iOS/ 泛型
特性(Attributes)
Access Control 權(quán)限控制的黑與白
基本運算符(Basic Operators)
基礎(chǔ)部分(The Basics)
閉包(Closures)
擴(kuò)展
泛型參數(shù)(Generic Parameters and Arguments)
訪問控制和 protected
語句(Statements)
模式(Patterns)
WWDC 里面的那個“大炮打氣球”
關(guān)于語言參考(About the Language Reference)
語法總結(jié)(Summary of the Grammar)
嵌套類型
類型(Types)
Swift 初見(A Swift Tour)
泛型
枚舉(Enumerations)
高級運算符
繼承
析構(gòu)過程
關(guān)于 Swift(About Swift)
訪問控制
類和結(jié)構(gòu)體
內(nèi)存安全
Swift 與 C 語言指針友好合作
協(xié)議
屬性(Properties)
可選類型完美解決占位問題
錯誤處理
字符串和字符(Strings and Characters)
聲明(Declarations)
自動引用計數(shù)
Swift 里的值類型與引用類型
表達(dá)式(Expressions)
Swift 文檔修訂歷史
造個類型不是夢-白話 Swift 類型創(chuàng)建
歡迎使用 Swift
詞法結(jié)構(gòu)(Lexical Structure)
集合類型(Collection Types)
下標(biāo)
方法(Methods)
可選鏈?zhǔn)秸{(diào)用
版本兼容性
類型轉(zhuǎn)換
構(gòu)造過程
The Swift Programming Language 中文版
函數(shù)(Functions)
Swift 教程
控制流(Control Flow)

泛型


1.0 翻譯:takalard 校對:lifedim

2.0 翻譯+校對: SergioChan

2.1 校對:shanks,2015-11-01

2.2:翻譯+校對:Lanford,2016-04-08 SketchK 2016-05-16

3.0:翻譯+校對:chenmingjia,2016-09-12 3.0.1,shanks,2016-11-13

3.1:翻譯:qhd,2017-04-10

4.0 翻譯+校對:kemchenj 2017-09-21

4.1 翻譯+校對:mylittleswift

本頁包含內(nèi)容:

泛型代碼讓你能夠根據(jù)自定義的需求,編寫出適用于任意類型、靈活可重用的函數(shù)及類型。它能讓你避免代碼的重復(fù),用一種清晰和抽象的方式來表達(dá)代碼的意圖。

泛型是 Swift 最強大的特性之一,許多 Swift 標(biāo)準(zhǔn)庫是通過泛型代碼構(gòu)建的。事實上,泛型的使用貫穿了整本語言手冊,只是你可能沒有發(fā)現(xiàn)而已。例如,Swift 的 ArrayDictionary 都是泛型集合。你可以創(chuàng)建一個 Int 數(shù)組,也可創(chuàng)建一個 String 數(shù)組,甚至可以是任意其他 Swift 類型的數(shù)組。同樣的,你也可以創(chuàng)建存儲任意指定類型的字典。

泛型所解決的問題

下面是一個標(biāo)準(zhǔn)的非泛型函數(shù) swapTwoInts(_:_:),用來交換兩個 Int 值:

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

這個函數(shù)使用輸入輸出參數(shù)(inout)來交換 ab 的值,請參考輸入輸出參數(shù)。

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)")
// 打印 “someInt is now 107, and anotherInt is now 3”

誠然,swapTwoInts(_:_:) 函數(shù)挺有用,但是它只能交換 Int 值,如果你想要交換兩個 String 值或者 Double 值,就不得不寫更多的函數(shù),例如 swapTwoStrings(_:_:)swapTwoDoubles(_:_:),如下所示:

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(_:_:) 的函數(shù)功能都是相同的,唯一不同之處就在于傳入的變量類型不同,分別是 IntStringDouble

在實際應(yīng)用中,通常需要一個更實用更靈活的函數(shù)來交換兩個任意類型的值,幸運的是,泛型代碼幫你解決了這種問題。(這些函數(shù)的泛型版本已經(jīng)在下面定義好了。)

注意

在上面三個函數(shù)中,ab 類型必須相同。如果 ab 類型不同,那它們倆就不能互換值。Swift 是類型安全的語言,所以它不允許一個 String 類型的變量和一個 Double 類型的變量互換值。試圖這樣做將導(dǎo)致編譯錯誤。

泛型函數(shù)

泛型函數(shù)可以適用于任何類型,下面的 swapTwoValues(_:_:) 函數(shù)是上面三個函數(shù)的泛型版本:

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

swapTwoValues(_:_:) 的函數(shù)主體和 swapTwoInts(_:_:) 函數(shù)是一樣的,它們只在第一行有點不同,如下所示:

func swapTwoInts(_ a: inout Int, _ b: inout Int)
func swapTwoValues<T>(_ a: inout T, _ b: inout T)

這個函數(shù)的泛型版本使用了占位類型名(在這里用字母 T 來表示)來代替實際類型名(例如 Int、StringDouble)。占位類型名沒有指明 T 必須是什么類型,但是它指明了 ab 必須是同一類型 T,無論 T 代表什么類型。只有 swapTwoValues(_:_:) 函數(shù)在調(diào)用時,才會根據(jù)所傳入的實際類型決定 T 所代表的類型。

泛型函數(shù)和非泛型函數(shù)的另外一個不同之處,在于這個泛型函數(shù)名(swapTwoValues(_:_:))后面跟著占位類型名(T),并用尖括號括起來(<T>)。這個尖括號告訴 Swift 那個 TswapTwoValues(_:_:) 函數(shù)定義內(nèi)的一個占位類型名,因此 Swift 不會去查找名為 T 的實際類型。

swapTwoValues(_:_:) 函數(shù)現(xiàn)在可以像 swapTwoInts(_:_:) 那樣調(diào)用,不同的是它能接受兩個任意類型的值,條件是這兩個值有著相同的類型。swapTwoValues(_:_:) 函數(shù)被調(diào)用時,T 所代表的類型都會由傳入的值的類型推斷出來。

在下面的兩個例子中,T 分別代表 IntString

var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt 現(xiàn)在 107, and anotherInt 現(xiàn)在 3

var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString 現(xiàn)在 "world", and anotherString 現(xiàn)在 "hello"

注意

上面定義的 swapTwoValues(_:_:) 函數(shù)是受 swap(_:_:) 函數(shù)啟發(fā)而實現(xiàn)的。后者存在于 Swift 標(biāo)準(zhǔn)庫,你可以在你的應(yīng)用程序中使用它。如果你在代碼中需要類似 swapTwoValues(_:_:) 函數(shù)的功能,你可以使用已存在的 swap(_:_:) 函數(shù)。

類型參數(shù)

在上面的 swapTwoValues(_:_:) 例子中,占位類型 T 是類型參數(shù)的一個例子。類型參數(shù)指定并命名一個占位類型,并且緊隨在函數(shù)名后面,使用一對尖括號括起來(例如 <T>)。

一旦一個類型參數(shù)被指定,你可以用它來定義一個函數(shù)的參數(shù)類型(例如 swapTwoValues(_:_:) 函數(shù)中的參數(shù) ab),或者作為函數(shù)的返回類型,還可以用作函數(shù)主體中的注釋類型。在這些情況下,類型參數(shù)會在函數(shù)調(diào)用時被實際類型所替換。(在上面的 swapTwoValues(_:_:) 例子中,當(dāng)函數(shù)第一次被調(diào)用時,TInt 替換,第二次調(diào)用時,被 String 替換。)

你可提供多個類型參數(shù),將它們都寫在尖括號中,用逗號分開。

命名類型參數(shù)

在大多數(shù)情況下,類型參數(shù)具有一個描述性名字,例如 Dictionary<Key, Value> 中的 KeyValue,以及 Array<Element> 中的 Element,這可以告訴閱讀代碼的人這些類型參數(shù)和泛型函數(shù)之間的關(guān)系。然而,當(dāng)它們之間沒有有意義的關(guān)系時,通常使用單個字母來命名,例如 TU、V,正如上面演示的 swapTwoValues(_:_:) 函數(shù)中的 T 一樣。

注意

請始終使用大寫字母開頭的駝峰命名法(例如 TMyTypeParameter)來為類型參數(shù)命名,以表明它們是占位類型,而不是一個值。

泛型類型

除了泛型函數(shù),Swift 還允許你定義泛型類型。這些自定義類、結(jié)構(gòu)體和枚舉可以適用于任何類型,類似于 ArrayDictionary。

這部分內(nèi)容將向你展示如何編寫一個名為 Stack (棧)的泛型集合類型。棧是一系列值的有序集合,和 Array 類似,但它相比 Swift 的 Array 類型有更多的操作限制。數(shù)組允許在數(shù)組的任意位置插入新元素或是刪除其中任意位置的元素。而棧只允許在集合的末端添加新的元素(稱之為棧)。類似的,棧也只能從末端移除元素(稱之為棧)。

注意

棧的概念已被 UINavigationController 類用來構(gòu)造視圖控制器的導(dǎo)航結(jié)構(gòu)。你通過調(diào)用 UINavigationControllerpushViewController(_:animated:) 方法來添加新的視圖控制器到導(dǎo)航棧,通過 popViewControllerAnimated(_:) 方法來從導(dǎo)航棧中移除視圖控制器。每當(dāng)你需要一個嚴(yán)格的“后進(jìn)先出”方式來管理集合,棧都是最實用的模型。

下圖展示了一個棧的入棧(push)和出棧(pop)的行為:

此處輸入圖片的描述

  1. 現(xiàn)在有三個值在棧中。
  2. 第四個值被壓入到棧的頂部。
  3. 現(xiàn)在有四個值在棧中,最近入棧的那個值在頂部。
  4. 棧中最頂部的那個值被移除出棧。
  5. 一個值移除出棧后,現(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)體在棧中使用一個名為 itemsArray 屬性來存儲值。Stack 提供了兩個方法:push(_:)pop(),用來向棧中壓入值以及從棧中移除值。這些方法被標(biāo)記為 mutating,因為它們需要修改結(jié)構(gòu)體的 items 數(shù)組。

上面的 IntStack 結(jié)構(gòu)體只能用于 Int 類型。不過,可以定義一個泛型 Stack 結(jié)構(gòu)體,從而能夠處理任意類型的值。

下面是相同代碼的泛型版本:

struct Stack<Element> {
    var items = [Element]()
 ? ?mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
}

注意,Stack 基本上和 IntStack 相同,只是用占位類型參數(shù) Element 代替了實際的 Int 類型。這個類型參數(shù)包裹在緊隨結(jié)構(gòu)體名的一對尖括號里(<Element>)。

Element 為待提供的類型定義了一個占位名。這種待提供的類型可以在結(jié)構(gòu)體的定義中通過 Element 來引用。在這個例子中,Element 在如下三個地方被用作占位符:

  • 創(chuàng)建 items 屬性,使用 Element 類型的空數(shù)組對其進(jìn)行初始化。
  • 指定 push(_:) 方法的唯一參數(shù) item 的類型必須是 Element 類型。
  • 指定 pop() 方法的返回值類型必須是 Element 類型。

由于 Stack 是泛型類型,因此可以用來創(chuàng)建 Swift 中任意有效類型的棧,就像 ArrayDictionary 那樣。

你可以通過在尖括號中寫出棧中需要存儲的數(shù)據(jù)類型來創(chuàng)建并初始化一個 Stack 實例。例如,要創(chuàng)建一個 String 類型的棧,可以寫成 Stack<String>()

var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// 棧中現(xiàn)在有 4 個字符串

下圖展示了 stackOfStrings 如何將這四個值入棧:

此處輸入圖片的描述

移除并返回棧頂部的值 "cuatro",即將其出棧:

let fromTheTop = stackOfStrings.pop()
// fromTheTop 的值為 "cuatro",現(xiàn)在棧中還有 3 個字符串

下圖展示了 stackOfStrings 如何將頂部的值出棧:

此處輸入圖片的描述

擴(kuò)展一個泛型類型

當(dāng)你擴(kuò)展一個泛型類型的時候,你并不需要在擴(kuò)展的定義中提供類型參數(shù)列表。原始類型定義中聲明的類型參數(shù)列表在擴(kuò)展中可以直接使用,并且這些來自原始類型中的參數(shù)名稱會被用作原始定義中類型參數(shù)的引用。

下面的例子擴(kuò)展了泛型類型 Stack,為其添加了一個名為 topItem 的只讀計算型屬性,它將會返回當(dāng)前棧頂端的元素而不會將其從棧中移除:

extension Stack {
    var topItem: Element? {
        return items.isEmpty ? nil : items[items.count - 1]
    }
}

topItem 屬性會返回一個 Element 類型的可選值。當(dāng)棧為空的時候,topItem 會返回 nil;當(dāng)棧不為空的時候,topItem 會返回 items 數(shù)組中的最后一個元素。

注意,這個擴(kuò)展并沒有定義一個類型參數(shù)列表。相反的,Stack 類型已有的類型參數(shù)名稱 Element,被用在擴(kuò)展中來表示計算型屬性 topItem 的可選類型。

計算型屬性 topItem 現(xiàn)在可以用來訪問任意 Stack 實例的頂端元素且不移除它:

if let topItem = stackOfStrings.topItem {
    print("The top item on the stack is \(topItem).")
}
// 打印 “The top item on the stack is tres.”

類型約束

swapTwoValues(_:_:) 函數(shù)和 Stack 類型可以作用于任何類型。不過,有的時候如果能將使用在泛型函數(shù)和泛型類型中的類型添加一個特定的類型約束,將會是非常有用的。類型約束可以指定一個類型參數(shù)必須繼承自指定類,或者符合一個特定的協(xié)議或協(xié)議組合。

例如,Swift 的 Dictionary 類型對字典的鍵的類型做了些限制。在字典的描述中,字典的鍵的類型必須是可哈希(hashable)的。也就是說,必須有一種方法能夠唯一地表示它。Dictionary 的鍵之所以要是可哈希的,是為了便于檢查字典是否已經(jīng)包含某個特定鍵的值。若沒有這個要求,Dictionary 將無法判斷是否可以插入或者替換某個指定鍵的值,也不能查找到已經(jīng)存儲在字典中的指定鍵的值。

為了實現(xiàn)這個要求,一個類型約束被強制加到 Dictionary 的鍵類型上,要求其鍵類型必須符合 Hashable 協(xié)議,這是 Swift 標(biāo)準(zhǔn)庫中定義的一個特定協(xié)議。所有的 Swift 基本類型(例如 StringInt、DoubleBool)默認(rèn)都是可哈希的。

當(dāng)你創(chuàng)建自定義泛型類型時,你可以定義你自己的類型約束,這些約束將提供更為強大的泛型編程能力。抽象概念,例如可哈希的,描述的是類型在概念上的特征,而不是它們的顯式類型。

類型約束語法

你可以在一個類型參數(shù)名后面放置一個類名或者協(xié)議名,并用冒號進(jìn)行分隔,來定義類型約束,它們將成為類型參數(shù)列表的一部分。對泛型函數(shù)添加類型約束的基本語法如下所示(作用于泛型類型時的語法與之相同):

func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
    // 這里是泛型函數(shù)的函數(shù)體部分
}

上面這個函數(shù)有兩個類型參數(shù)。第一個類型參數(shù) T,有一個要求 T 必須是 SomeClass 子類的類型約束;第二個類型參數(shù) U,有一個要求 U 必須符合 SomeProtocol 協(xié)議的類型約束。

類型約束實踐

這里有個名為 findIndex(ofString:in:) 的非泛型函數(shù),該函數(shù)的功能是在一個 String 數(shù)組中查找給定 String 值的索引。若查找到匹配的字符串,findIndex(ofString:in:) 函數(shù)返回該字符串在數(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ù)可以用于查找字符串?dāng)?shù)組中的某個字符串:

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”

如果只能查找字符串在數(shù)組中的索引,用處不是很大。不過,你可以用占位類型 T 替換 String 類型來寫出具有相同功能的泛型函數(shù) findIndex(_:_:)。

下面展示了 findIndex(ofString:in:) 函數(shù)的泛型版本 findIndex(ofString:in:)。請注意這個函數(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 類型都可以用等式符(==)進(jìn)行比較。比如說,如果你創(chuàng)建一個自定義的類或結(jié)構(gòu)體來表示一個復(fù)雜的數(shù)據(jù)模型,那么 Swift 無法猜到對于這個類或結(jié)構(gòu)體而言“相等”意味著什么。正因如此,這部分代碼無法保證適用于每個可能的類型 T,當(dāng)你試圖編譯這部分代碼時會出現(xiàn)相應(yīng)的錯誤。

不過,所有的這些并不會讓我們無從下手。Swift 標(biāo)準(zhǔn)庫中定義了一個 Equatable 協(xié)議,該協(xié)議要求任何遵循該協(xié)議的類型必須實現(xiàn)等式符(==)及不等符(!=),從而能對該類型的任意兩個值進(jìn)行比較。所有的 Swift 標(biāo)準(zhǔn)類型自動支持 Equatable 協(xié)議。

任何 Equatable 類型都可以安全地使用在 findIndex(of:in:) 函數(shù)中,因為其保證支持等式操作符。為了說明這個事實,當(dāng)你定義一個函數(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 的類型,如 DoubleString

let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25])
// doubleIndex 類型為 Int?,其值為 nil,因為 9.3 不在數(shù)組中
let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"])
// stringIndex 類型為 Int?,其值為 2

關(guān)聯(lián)類型

定義一個協(xié)議時,有的時候聲明一個或多個關(guān)聯(lián)類型作為協(xié)議定義的一部分將會非常有用。關(guān)聯(lián)類型為協(xié)議中的某個類型提供了一個占位名(或者說別名),其代表的實際類型在協(xié)議被采納時才會被指定。你可以通過 associatedtype 關(guān)鍵字來指定關(guān)聯(lián)類型。

關(guān)聯(lián)類型實踐

下面例子定義了一個 Container 協(xié)議,該協(xié)議定義了一個關(guān)聯(lián)類型 Item

protocol Container {
    associatedtype Item
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

Container 協(xié)議定義了三個任何采納了該協(xié)議的類型(即容器)必須提供的功能:

  • 必須可以通過 append(_:) 方法添加一個新元素到容器里。
  • 必須可以通過 count 屬性獲取容器中元素的數(shù)量,并返回一個 Int 值。
  • 必須可以通過索引值類型為 Int 的下標(biāo)檢索到容器中的每一個元素。

這個協(xié)議沒有指定容器中元素該如何存儲,以及元素必須是何種類型。這個協(xié)議只指定了三個任何遵從 Container 協(xié)議的類型必須提供的功能。遵從協(xié)議的類型在滿足這三個條件的情況下也可以提供其他額外的功能。

任何遵從 Container 協(xié)議的類型必須能夠指定其存儲的元素的類型,必須保證只有正確類型的元素可以加進(jìn)容器中,必須明確通過其下標(biāo)返回的元素的類型。

為了定義這三個條件,Container 協(xié)議需要在不知道容器中元素的具體類型的情況下引用這種類型。Container 協(xié)議需要指定任何通過 append(_:) 方法添加到容器中的元素和容器中的元素是相同類型,并且通過容器下標(biāo)返回的元素的類型也是這種類型。

為了達(dá)到這個目的,Container 協(xié)議聲明了一個關(guān)聯(lián)類型 Item,寫作 associatedtype Item。這個協(xié)議無法定義 Item 是什么類型的別名,這個信息將留給遵從協(xié)議的類型來提供。盡管如此,Item 別名提供了一種方式來引用 Container 中元素的類型,并將之用于 append(_:) 方法和下標(biāo),從而保證任何 Container 的行為都能夠正如預(yù)期地被執(zhí)行。

下面是先前的非泛型的 IntStack 類型,這一版本采納并符合了 Container 協(xié)議:

struct IntStack: Container {
    // IntStack 的原始實現(xiàn)部分
    var items = [Int]()
    mutating func push(_ item: Int) {
        items.append(item)
    }
    mutating func pop() -> Int {
        return items.removeLast()
    }
    // Container 協(xié)議的實現(xiàn)部分
    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 結(jié)構(gòu)體實現(xiàn)了 Container 協(xié)議的三個要求,其原有功能也不會和這些要求相沖突。

此外,IntStack 在實現(xiàn) Container 的要求時,指定 ItemInt 類型,即 typealias Item = Int,從而將 Container 協(xié)議中抽象的 Item 類型轉(zhuǎn)換為具體的 Int 類型。

由于 Swift 的類型推斷,你實際上不用在 IntStack 的定義中聲明 ItemInt。因為 IntStack 符合 Container 協(xié)議的所有要求,Swift 只需通過 append(_:) 方法的 item 參數(shù)類型和下標(biāo)返回值的類型,就可以推斷出 Item 的具體類型。事實上,如果你在上面的代碼中刪除了 typealias Item = Int 這一行,一切仍舊可以正常工作,因為 Swift 清楚地知道 Item 應(yīng)該是哪種類型。

你也可以讓泛型 Stack 結(jié)構(gòu)體遵從 Container 協(xié)議:

struct Stack<Element>: Container {
    // Stack<Element> 的原始實現(xiàn)部分
    var items = [Element]()
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
    // Container 協(xié)議的實現(xiàn)部分
    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ù)和下標(biāo)的返回類型。Swift 可以據(jù)此推斷出 Element 的類型即是 Item 的類型。

通過擴(kuò)展一個存在的類型來指定關(guān)聯(lián)類型

通過擴(kuò)展添加協(xié)議一致性中描述了如何利用擴(kuò)展讓一個已存在的類型符合一個協(xié)議,這包括使用了關(guān)聯(lián)類型的協(xié)議。

Swift 的 Array 類型已經(jīng)提供 append(_:) 方法,一個 count 屬性,以及一個接受 Int 類型索引值的下標(biāo)用以檢索其元素。這三個功能都符合 Container 協(xié)議的要求,也就意味著你只需簡單地聲明 Array 采納該協(xié)議就可以擴(kuò)展 Array,使其遵從 Container 協(xié)議。你可以通過一個空擴(kuò)展來實現(xiàn)這點,正如通過擴(kuò)展采納協(xié)議中的描述:

extension Array: Container {}

如同上面的泛型 Stack 結(jié)構(gòu)體一樣,Arrayappend(_:) 方法和下標(biāo)確保了 Swift 可以推斷出 Item 的類型。定義了這個擴(kuò)展后,你可以將任意 Array 當(dāng)作 Container 來使用。

給關(guān)聯(lián)類型添加約束

你可以給協(xié)議里的關(guān)聯(lián)類型添加類型注釋,讓遵守協(xié)議的類型必須遵循這個約束條件。例如,下面的代碼定義了一個 Item 必須遵循 EquatableContainer 類型:

protocol Container {
    associatedtype Item: Equatable
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

為了遵守了 Container 協(xié)議,Item 類型也必須遵守 Equatable 協(xié)議。

在關(guān)聯(lián)類型約束里使用協(xié)議

協(xié)議可以作為它自身的要求出現(xiàn)。例如,有一個協(xié)議細(xì)化了 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)類型,就像上邊例子中 ContainerItem 類型一樣。Suffix 擁有兩個約束:它必須遵循 SuffixableContainer 協(xié)議(就是當(dāng)前定義的協(xié)議),以及它的 Item 類型必須是和容器里的 Item 類型相同。Item 的約束是一個 where 分句,它在下面帶有泛型 Where 分句的擴(kuò)展中有討論。

這里有一個來自閉包的循環(huán)強引用的 Stack 類型的擴(kuò)展,它添加了對 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

在上面的例子中,SuffixStack 的關(guān)聯(lián)類型,也就是 Stack ,所以 Stack 的后綴運算返回另一個 Stack 。另外,遵循 SuffixableContainer 的類型可以擁有一個與它自己不同的 Suffix 類型——也就是說后綴運算可以返回不同的類型。比如說,這里有一個非泛型 IntStack 類型的擴(kuò)展,它添加了 SuffixableContainer 遵循,使用 Stack<Int> 作為它的后綴類型而不是 IntStack

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>.
}

泛型 Where 語句

類型約束讓你能夠為泛型函數(shù),下標(biāo),類型的類型參數(shù)定義一些強制要求。

為關(guān)聯(lián)類型定義約束也是非常有用的。你可以在參數(shù)列表中通過 where 子句為關(guān)聯(lián)類型定義約束。你能通過 where 子句要求一個關(guān)聯(lián)類型遵從某個特定的協(xié)議,以及某個特定的類型參數(shù)和關(guān)聯(lián)類型必須類型相同。你可以通過將 where 關(guān)鍵字緊跟在類型參數(shù)列表后面來定義 where 子句,where 子句后跟一個或者多個針對關(guān)聯(lián)類型的約束,以及一個或多個類型參數(shù)和關(guān)聯(lián)類型間的相等關(guān)系。你可以在函數(shù)體或者類型的大括號之前添加 where 子句。

下面的例子定義了一個名為 allItemsMatch 的泛型函數(shù),用來檢查兩個 Container 實例是否包含相同順序的相同元素。如果所有的元素能夠匹配,那么返回 true,否則返回 false。

被檢查的兩個 Container 可以不是相同類型的容器(雖然它們可以相同),但它們必須擁有相同類型的元素。這個要求通過一個類型約束以及一個 where 子句來表示:

func allItemsMatch<C1: Container, C2: Container>
    (_ someContainer: C1, _ anotherContainer: C2) -> Bool
    where C1.Item == C2.Item, C1.Item: Equatable {

        // 檢查兩個容器含有相同數(shù)量的元素
        if someContainer.count != anotherContainer.count {
            return false
        }

        // 檢查每一對元素是否相等
        for i in 0..<someContainer.count {
            if someContainer[i] != anotherContainer[i] {
                return false
            }
        }

        // 所有元素都匹配,返回 true
        return true
}

這個函數(shù)接受 someContaineranotherContainer 兩個參數(shù)。參數(shù) someContainer 的類型為 C1,參數(shù) anotherContainer 的類型為 C2。C1C2 是容器的兩個占位類型參數(shù),函數(shù)被調(diào)用時才能確定它們的具體類型。

這個函數(shù)的類型參數(shù)列表還定義了對兩個類型參數(shù)的要求:

  • C1 必須符合 Container 協(xié)議(寫作 C1: Container)。
  • C2 必須符合 Container 協(xié)議(寫作 C2: Container)。
  • C1Item 必須和 C2Item 類型相同(寫作 C1.Item == C2.Item)。
  • C1Item 必須符合 Equatable 協(xié)議(寫作 C1.Item: Equatable)。

第三個和第四個要求被定義為一個 where 子句,寫在關(guān)鍵字 where 后面,它們也是泛型函數(shù)類型參數(shù)列表的一部分。

這些要求意味著:

  • someContainer 是一個 C1 類型的容器。
  • anotherContainer 是一個 C2 類型的容器。
  • someContaineranotherContainer 包含相同類型的元素。
  • someContainer 中的元素可以通過不等于操作符(!=)來檢查它們是否彼此不同。

第三個和第四個要求結(jié)合起來意味著 anotherContainer 中的元素也可以通過 != 操作符來比較,因為它們和 someContainer 中的元素類型相同。

這些要求讓 allItemsMatch(_:_:) 函數(shù)能夠比較兩個容器,即使它們的容器類型不同。

allItemsMatch(_:_:) 函數(shù)首先檢查兩個容器是否擁有相同數(shù)量的元素,如果它們的元素數(shù)量不同,那么一定不匹配,函數(shù)就會返回 false

進(jìn)行這項檢查之后,通過 for-in 循環(huán)和半閉區(qū)間操作符(..<)來迭代每個元素,檢查 someContainer 中的元素是否不等于 anotherContainer 中的對應(yīng)元素。如果兩個元素不相等,那么兩個容器不匹配,函數(shù)返回 false。

如果循環(huán)體結(jié)束后未發(fā)現(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.")
}
// 打印 “All items match.”

上面的例子創(chuàng)建了一個 Stack 實例來存儲一些 String 值,然后將三個字符串壓入棧中。這個例子還通過數(shù)組字面量創(chuàng)建了一個 Array 實例,數(shù)組中包含同棧中一樣的三個字符串。即使棧和數(shù)組是不同的類型,但它們都遵從 Container 協(xié)議,而且它們都包含相同類型的值。因此你可以用這兩個容器作為參數(shù)來調(diào)用 allItemsMatch(_:_:) 函數(shù)。在上面的例子中,allItemsMatch(_:_:) 函數(shù)正確地顯示了這兩個容器中的所有元素都是相互匹配的。

具有泛型 Where 子句的擴(kuò)展

你也可以使用泛型 where 子句作為擴(kuò)展的一部分?;谝郧暗睦?,下面的示例擴(kuò)展了泛型 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(_:) 里面使用了 == 運算符,但是 Stack 的定義沒有要求它的元素是符合 Equatable 協(xié)議的,所以使用 == 運算符導(dǎo)致編譯時錯誤。使用泛型 where 子句可以為擴(kuò)展添加新的條件,因此只有當(dāng)棧中的元素符合 Equatable 協(xié)議時,擴(kuò)展才會添加 isTop(_:) 方法。

以下是 isTop(_:) 方法的調(diào)用方式:

if stackOfStrings.isTop("tres") {
    print("Top element is tres.")
} else {
    print("Top element is something else.")
}
// 打印 "Top element is tres."

如果嘗試在其元素不符合 Equatable 協(xié)議的棧上調(diào)用 isTop(_:) 方法,則會收到編譯時錯誤。

struct NotEquatable { }
var notEquatableStack = Stack<NotEquatable>()
let notEquatableValue = NotEquatable()
notEquatableStack.push(notEquatableValue)
notEquatableStack.isTop(notEquatableValue)  // 報錯

你可以使用泛型 where 子句去擴(kuò)展一個協(xié)議?;谝郧暗氖纠旅娴氖纠龜U(kuò)展了 Container 協(xié)議,添加一個 startsWith(_:) 方法。

extension Container where Item: Equatable {
    func startsWith(_ item: Item) -> Bool {
        return count >= 1 && self[0] == item
    }
}

這個 startsWith(_:) 方法首先確保容器至少有一個元素,然后檢查容器中的第一個元素是否與給定的元素相等。任何符合 Container 協(xié)議的類型都可以使用這個新的 startsWith(_:) 方法,包括上面使用的棧和數(shù)組,只要容器的元素是符合 Equatable 協(xié)議的。

if [9, 9, 9].startsWith(42) {
    print("Starts with 42.")
} else {
    print("Starts with something else.")
}
// 打印 "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())
// 打印 "648.9"

此示例將一個 average() 方法添加到 Item 類型為 Double 的容器中。此方法遍歷容器中的元素將其累加,并除以容器的數(shù)量計算平均值。它將數(shù)量從 Int 轉(zhuǎn)換為 Double 確保能夠進(jìn)行浮點除法。

就像可以在其他地方寫泛型 where 子句一樣,你可以在一個泛型 where 子句中包含多個條件作為擴(kuò)展的一部分。用逗號分隔列表中的每個條件。

具有泛型 Where 子句的關(guān)聯(lián)類型

你可以在關(guān)聯(lián)類型后面加上具有泛型 where 的字句。例如,建立一個包含迭代器(Iterator)的容器,就像是標(biāo)準(zhǔn)庫中使用的 Sequence 協(xié)議那樣。你應(yīng)該這么寫:

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() 則提供了容器的迭代器的訪問接口。

一個協(xié)議繼承了另一個協(xié)議,你通過在協(xié)議聲明的時候,包含泛型 where 子句,來添加了一個約束到被繼承協(xié)議的關(guān)聯(lián)類型。例如,下面的代碼聲明了一個 ComparableContainer 協(xié)議,它要求所有的 Item 必須是 Comparable 的。

protocol ComparableContainer: Container where Item: Comparable { }

泛型下標(biāo)

下標(biāo)能夠是泛型的,他們能夠包含泛型 where 子句。你可以把占位符類型的名稱寫在 subscript 后面的尖括號里,在下標(biāo)代碼體開始的標(biāo)志的花括號之前寫下泛型 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é)議的擴(kuò)展添加了一個下標(biāo)方法,接收一個索引的集合,返回每一個索引所在的值的數(shù)組。這個泛型下標(biāo)的約束如下:

這個 Container 協(xié)議的擴(kuò)展添加了一個下標(biāo):下標(biāo)是一個序列的索引,返回的則是索引所在的項目的值所構(gòu)成的數(shù)組。這個泛型下標(biāo)的約束如下:

  • 在尖括號中的泛型參數(shù) Indices,必須是符合標(biāo)準(zhǔn)庫中的 Sequence 協(xié)議的類型。
  • 下標(biāo)使用的單一的參數(shù),indices,必須是 Indices 的實例。
  • 泛型 where 子句要求 Sequence(Indices)的迭代器,其所有的元素都是 Int 類型。這樣就能確保在序列(Sequence)中的索引和容器(Container)里面的索引類型是一致的。

綜合一下,這些約束意味著,傳入到 indices 下標(biāo),是一個整型的序列。