這里的教程為Swift官方教程中文版。

泛型

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

泛型是 Swift 最強(qiáng)大的特性之一,很多 Swift 標(biāo)準(zhǔn)庫是基于泛型代碼構(gòu)建的。實(shí)際上,即使你沒有意識到,你也一直在語言指南中使用泛型。例如,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ù)很實(shí)用,但它只能作用于 Int 類型。如果你想交換兩個 String 類型值,或者 Double 類型值,你必須編寫對應(yīng)的函數(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
}

你可能注意到了,swapTwoInts(_:_:‘)、swapTwoStrings(_:_:)swapTwoDoubles(_:_:) 函數(shù)體是一樣的,唯一的區(qū)別是它們接受的參數(shù)類型(Int、StringDouble)。

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

注意

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

泛型函數(shù)

泛型函數(shù)可適用于任意類型,下面是函數(shù) swapTwoInts(_:_:) 的泛型版本,命名為 swapTwoValues(_:_:)

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

swapTwoValues(_:_:)swapTwoInts(_:_:) 函數(shù)體內(nèi)容相同,它們只在第一行不同,如下所示:

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

泛型版本的函數(shù)使用占位符類型名(這里叫做 T ),而不是 實(shí)際類型名(例如 Int、StringDouble),占位符類型名并不關(guān)心 T 具體的類型,但它要求 ab 必須是相同的類型,T 的實(shí)際類型由每次調(diào)用 swapTwoValues(_:_:) 來決定。

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

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,anotherInt 現(xiàn)在是 3

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

注意

上面定義的 swapTwoValues(_:_:) 函數(shù)是受 swap(_:_:) 函數(shù)啟發(fā)而實(shí)現(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)用時被實(shí)際類型所替換。(在上面的 swapTwoValues(_:_:) 例子中,當(dāng)函數(shù)第一次被調(diào)用時,TInt 替換,第二次調(diào)用時,被 String 替換。)

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

命名類型參數(shù)

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

注意

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

泛型類型

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

本節(jié)將向你展示如何編寫一個名為 Stack(棧)的泛型集合類型。棧是值的有序集合,和數(shù)組類似,但比數(shù)組有更嚴(yán)格的操作限制。數(shù)組允許在其中任意位置插入或是刪除元素。而棧只允許在集合的末端添加新的元素(稱之為入棧)。類似的,棧也只能從末端移除元素(稱之為出棧)。

注意

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

下圖展示了入棧(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)體在棧中使用一個名為 items 的數(shù)組屬性來存儲值。棧提供了兩個方法:push(_:)pop(),用來向棧中壓入值以及從棧中移除值。這些方法被標(biāo)記為 mutating,因?yàn)樗鼈冃枰薷慕Y(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 代替了實(shí)際的 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 實(shí)例。例如,要創(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 個字符串

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

泛型擴(kuò)展

當(dāng)對泛型類型進(jìn)行擴(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 實(shí)例的頂端元素且不移除它:

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

泛型類型的擴(kuò)展,還可以包括類型擴(kuò)展需要額外滿足的條件,從而對類型添加新功能,這一部分將在 具有泛型 Where 子句的擴(kuò)展 中進(jìn)行討論。

類型約束

swapTwoValues(_:_:) 函數(shù)和 Stack 適用于任意類型。不過,如果能對泛型函數(shù)或泛型類型中添加特定的類型約束,這將在某些情況下非常有用。類型約束指定類型參數(shù)必須繼承自指定類、遵循特定的協(xié)議或協(xié)議組合。

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

這個要求通過 Dictionary 鍵類型上的類型約束實(shí)現(xiàn),它指明了鍵必須遵循 Swift 標(biāo)準(zhǔn)庫中定義的 Hashable 協(xié)議。所有 Swift 的基本類型(例如 String、IntDoubleBool)默認(rèn)都是可哈希的。

當(dāng)自定義泛型類型時,你可以定義你自己的類型約束,這些約束將提供更為強(qiáng)大的泛型編程能力。像 可哈希(hashable) 這種抽象概念根據(jù)它們的概念特征來描述類型,而不是它們的具體類型。

類型約束語法

在一個類型參數(shù)名后面放置一個類名或者協(xié)議名,并用冒號進(jìn)行分隔,來定義類型約束。下面將展示泛型函數(shù)約束的基本語法(與泛型類型的語法相同):

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

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

類型約束實(shí)踐

這里有個名為 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(of:in:)。請注意這個函數(shù)返回值的類型仍然是 Int?,這是因?yàn)楹瘮?shù)返回的是一個可選的索引數(shù),而不是從數(shù)組中得到的一個可選值。需要提醒的是,這個函數(shù)無法通過編譯,原因?qū)⒃诤竺嬲f明:

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)行比較。例如,如果你自定義類或結(jié)構(gòu)體來描述復(fù)雜的數(shù)據(jù)模型,對于這個類或結(jié)構(gòu)體而言,Swift 無法明確知道“相等”意味著什么。正因如此,這部分代碼無法保證適用于任意類型 T,當(dāng)你試圖編譯這部分代碼時就會出現(xiàn)相應(yīng)的錯誤。

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

遵循 Equatable 協(xié)議的類型都可以安全地用于 findIndex(of:in:) 函數(shù),因?yàn)槠浔WC支持等式操作符。為了說明這個事情,當(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,因?yàn)?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é)議中的某個類型提供了一個占位符名稱,其代表的實(shí)際類型在協(xié)議被遵循時才會被指定。關(guān)聯(lián)類型通過 associatedtype 關(guān)鍵字來指定。

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

下面例子定義了一個 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é)議的類型必須能夠指定其存儲的元素的類型。具體來說,它必須確保添加到容器內(nèi)的元素以及下標(biāo)返回的元素類型是正確的。

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

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

這是前面非泛型版本 IntStack 類型,使其遵循 Container 協(xié)議:

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

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

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

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

struct Stack<Element>: Container {
    // Stack<Element> 的原始實(shí)現(xiàn)部分
    var items = [Element]()
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
    // Container 協(xié)議的實(shí)現(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ò)展現(xiàn)有類型來指定關(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 遵循Container 協(xié)議,就可以擴(kuò)展 Array,使其遵從 Container 協(xié)議。你可以通過一個空擴(kuò)展來實(shí)現(xiàn)這點(diǎn),正如通過擴(kuò)展采納協(xié)議中的描述:

extension Array: Container {}

Arrayappend(_:) 方法和下標(biāo)確保了 Swift 可以推斷出 Item 具體類型。定義了這個擴(kuò)展后,你可以將任意 Array 當(dāng)作 Container 來使用。

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

你可以在協(xié)議里給關(guān)聯(lián)類型添加約束來要求遵循的類型滿足約束。例如,下面的代碼定義了 Container 協(xié)議, 要求關(guān)聯(lián)類型 Item 必須遵循 Equatable 協(xié)議:

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 類型的實(shí)例里。

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ò)展 中有討論。

這是上面 泛型類型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
    }
    // 推斷 suffix 結(jié)果是Stack。
}
var stackOfInts = Stack<Int>()
stackOfInts.append(10)
stackOfInts.append(20)
stackOfInts.append(30)
let suffix = stackOfInts.suffix(2)
// suffix 包含 20 和 30

在上面的例子中,SuffixStack 的關(guān)聯(lián)類型,也是 Stack ,所以 Stack 的后綴運(yùn)算返回另一個 Stack 。另外,遵循 SuffixableContainer 的類型可以擁有一個與它自己不同的 Suffix 類型——也就是說后綴運(yùn)算可以返回不同的類型。比如說,這里有一個非泛型 IntStack 類型的擴(kuò)展,它遵循了 SuffixableContainer 協(xié)議,使用 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
    }
    // 推斷 suffix 結(jié)果是 Stack<Int>。
}

泛型 Where 語句

類型約束 讓你能夠?yàn)榉盒秃瘮?shù)、下標(biāo)、類型的類型參數(shù)定義一些強(qiáng)制要求。

對關(guān)聯(lián)類型添加約束通常是非常有用的。你可以通過定義一個泛型 where 子句來實(shí)現(xià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 實(shí)例是否包含相同順序的相同元素。如果所有的元素能夠匹配,那么返回 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)。

前兩個要求定義在函數(shù)的類型形式參數(shù)列表里,后兩個要求定義在了函數(shù)的泛型 where 分句中。

這些要求意味著:

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

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

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

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

進(jìn)行這項(xiàng)檢查之后,通過 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 實(shí)例來存儲 String 值,然后將三個字符串壓棧。這個例子還通過數(shù)組字面量創(chuàng)建了一個 Array 實(shí)例,數(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(_:) 里面使用了 == 運(yùn)算符,但是 Stack 的定義沒有要求它的元素是符合 Equatable 協(xié)議的,所以使用 == 運(yùn)算符導(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é)議?;谝郧暗氖纠?,下面的示例擴(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)行浮點(diǎ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 子句要求:無論迭代器是什么類型,迭代器中的元素類型,必須和容器項(xiàng)目的類型保持一致。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)代碼塊花括號前寫 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)的約束如下:

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

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

? 協(xié)議 不透明類型 ?
?