泛型

泛型(generic)是 Swift 一個重要的特性,可以讓你自定義出一個適用任意型別的函式及型別。可以避免重複的程式碼且清楚的表達程式碼的目的。

許多 Swift 標準函式庫就是經由泛型程式碼建構出來的,像是陣列(Array)和字典(Dictionary)都是泛型的,你可以宣告一個[Int]陣列,也可以宣告一個[String]陣列。同樣地,你也可以宣告任意指定型別的字典。

你可以將泛型使用在函式、列舉、結構及類別上。

泛型能解決的問題

以下是一個可以利用泛型來簡化程式碼的例子:

// 定義一個將兩個整數變數的值互換的函式
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

// 宣告兩個整數變數 並當做參數傳入函式
var oneInt = 12
var anotherInt = 500
swapTwoInts(&oneInt, &anotherInt)

// 印出:互換後的 oneInt 為 500,anotherInt 為 12
print("互換後的 oneInt 為 \(oneInt),anotherInt 為 \(anotherInt)")

// 與上面定義的函式功能相同 只是這時互換的變數型別為字串
func swapTwoStrings(_ a: inout String, _ b: inout String) {
    let temporaryA = a
    a = b
    b = temporaryA
}

由上述程式可以看到,兩個函式的功能完全一樣,唯一不同的只有傳入參數的型別,這種情況便可以使用泛型來簡化。

泛型函式

根據前面提到的兩個功能完全一樣的函式,以下使用泛型來定義一個適用任意型別的函式:

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

上述程式中的函式使用了佔位型別名稱(placeholder type name,習慣以字母T來表示)來代替實際型別名稱(像是IntDoubleString)。

可以注意到函式名稱後面緊接著一組角括號<>,且包著T。這代表角括號內的T是函式定義的一個佔位型別名稱,因此 Swift 不會去查找名稱為T的實際型別。

定義佔位型別名稱時不會明確表示T是什麼型別,但參數ab都必須是這個T型別。而只有當這個函式被呼叫時,才會根據傳入參數的實際型別,來決定T所代表的型別。

這時便可以使用這個泛型函式,如下:

//  首先是兩個整數
var oneInt2 = 12
var anotherInt2 = 320
swapTwoValues(&oneInt2, &anotherInt2)

// 再來是兩個字串
var oneString = "Hello"
var anotherString = "world"
swapTwoValues(&oneString, &anotherString)

型別參數

前面提到的swapTwoValues(_:_:)中,佔位型別名稱T是型別參數的一個例子。

型別參數會指定並命名一個佔位型別,且會緊跟在函式名稱後面使用一組角括號<>包起來。當一個型別參數被指定後,就可以用來定義一個函式的參數型別函式的返回值型別或是函式內的型別標註

型別參數可以指定一個或一個以上,使用多個時以逗號,隔開。

命名型別參數

在一般情況下,型別參數會指定為一個有描述性的名字,像是Dictionary<Key, Value>中的KeyValue,或是Array<Element>中的Element,用來明顯表示這些型別參數與泛型函式之間的關係。而當無法有意義的描述型別參數時,通常會使用單一字母來命名,像是TUV

Hint
  • 通常會使用大駝峰式命名法(像是TMyTypeParameter)來為型別參數命名,以表示他們是佔位型別,而不是一個值。

泛型型別

除了泛型函式,你也可以定義一個泛型型別。你可以定義在列舉、結構或類別上,類似陣列(Array)和字典(Dictionary)。

以下會定義一個堆疊(Stack)的泛型集合型別來當做一個例子。堆疊的運作方式有點像陣列,可以增加(push)一個元素到陣列最後一員,也可以從陣列中取出(pop)最後一個元素。

// 定義一個泛型結構 Stack 其佔位型別參數命名為 Element
struct Stack<Element> {
    // 將型別參數用於型別標註 設置一個型別為 [Element] 的空陣列
    var items = [Element]()

    // 型別參數用於方法的參數型別 方法功能是增加一個元素到陣列最後一員
    mutating func push(_ item: Element) {
        items.append(item)
    }

    // 型別參數用於方法的返回值型別 方法功能是移除陣列的最後一個元素
    mutating func pop() -> Element {
        return items.removeLast()
    }
}

上述定義的結構中可以看到,指定Element為佔位型別參數後,便可在結構中作為型別標註方法的參數型別方法的返回值型別,而因為必須修改結構的內容,所以方法都必須加上mutating

接著就可以使用這個剛定義好的Stack型別,如下:

// 先宣告一個空的 Stack 這時才決定其內元素的型別為 String
var stackOfStrings = Stack<String>()

// 依序放入三個字串
stackOfStrings.push("one")
stackOfStrings.push("two")
stackOfStrings.push("three")

// 然後移除掉最後一個元素 即字串 "three"
stackOfStrings.pop()

// 現在這個 Stack 還有兩個元素 分別為 one 及 two

擴展一個泛型型別

當你擴展一個泛型型別時,不需要在擴展的定義中提供型別參數列表,原型別已經定義的型別參數列表(如前面提到的Stack定義的Element)可以直接在擴展中使用。

以下為堆疊(Stack)擴展一個名稱為topItem的唯讀計算屬性,它會返回這個堆疊的最後一個元素,且不會將其移除:

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

上述程式可以看到,擴展中可以直接使用Element。而返回值為一個可選值,所以底下使用可選綁定來取得最後一個元素:

if let topItem = stackOfStrings.topItem {
    // 印出:最後一個元素為 two
    print("最後一個元素為 \(topItem)")
}

型別約束

有時在定義一個泛型函式或泛型型別時,會需要為這個泛型型別參數增加一些限制,可能是指定型別參數必須繼承自指定的類別,或是符合一個特定的協定,也就是型別約束(type constraint)。

像是 Swift 內建的字典(Dictionary)便對字典的鍵的型別作了些限制。字典的鍵的型別必須是可雜湊的(hashable),也就是必須只有唯一一種方式可以表示這個鍵。

而實際上為了實現這個限制,字典的鍵的型別符合了Hashable協定。Hashable是 Swift 標準函式庫中定義的一個特定協定,所有 Swift 的基本型別(像是IntDoubleBoolString)預設都是可雜湊的(hashable)。

型別約束語法

你可以在一個型別參數名稱後面加上冒號:並緊接著一個類別或是協定來做為型別約束,它們會成為型別參數列表的一部分,例子如下(泛型型別也是一樣方式):

func 泛型函式名稱<T: 某個類別, U: 某個協定>(參數: T, 另一個參數: U) {
    函式內部的程式
}

上述定義中可以看到,T型別參數必須繼承自某個類別U型別參數則必須遵循某個協定

使用型別約束

以下會定義一個函式,兩個參數分別為一個陣列及一個值,函式的功能是尋找第一個參數陣列中是否有另一個參數值,如果有就返回這個值在陣列中的索引值,找不到則返回nil

這個函式的型別約束會使用到另一個 Swift 標準函式庫中的Equatable協定,這個協定要求任何遵循該協定的型別必須實作==!=,進而可以對該型別的任意兩個值進行比較。(所有的 Swift 標準型別預設都符合Equatable協定。)

func findIndex<T: Equatable>(
  of valueToFind: T, in array:[T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

// 首先找看看 [Double] 陣列的值
let doubleIndex = findIndex(of: 9.2, in: [68.9, 55.66, 10.05])
// 因為 9.2 不在陣列中 所以返回 nil

// 接著找 [String] 陣列的值
let stringIndex = findIndex(of: "Kevin", in: ["Adam", "Kevin", "Jess"])
// Kevin 為陣列中第 2 個值 所以會返回 1

上述程式中,為這個泛型函式的泛型型別加上泛型約束,該型別必須遵循Equatable協定才能使用這個函式,藉此約束了無法彼此比較(也就是沒有實作==!=)的型別來使用。

關聯型別

關聯型別(associated type)表示會為協定中的某個型別提供一個佔位名稱(placeholder name),其代表的實際型別會在協定被遵循時才會被指定。使用associatedtype關鍵字來指定一個關聯型別。

底下是一個例子,定義一個協定Container,協定中定義了一個關聯型別Item

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

上述程式中可以看到,協定定義的方法append()參數的型別下標的返回值型別都是Item,目前仍是佔位名稱,實際型別要等到這個協定被遵循後才會被指定。

接著我們將前面定義的堆疊(Stack)遵循這個協定Container,在實作協定Container的全部功能後,Swift 會自動推斷Item的型別就是Element,如下:

struct NewStack<Element>: Container {
    // Stack<Element> 原實作的內容
    var items = [Element]()
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }

    // 原本應該要寫 typealias 
    // 但因為 Swift 會自動推斷型別 所以下面這行可以省略
    // typealias Item = Element

    // 協定 Container 實作的內容
    mutating func append(_ item: Element) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Element {
        return items[i]
    }
}

經由擴展一個已存在的型別來設置關聯型別

前面章節有提過,可以利用擴展來讓一個已存在的型別符合協定,使用了關聯型別的協定也一樣可以。

Swift 內建的陣列(Array)型別恰恰好已經有前面提過的協定Container需要實作的功能(分別是方法append()、屬性count及下標返回一個依索引值取得的元素)。所以現在可以很簡單的利用一個空的擴展來讓Array遵循這個協定,如下:

extension Array: Container {}

關聯型別使用型別標註

你可以為協定中的關聯型別增加一個型別標註,讓這個關聯型別也必須遵循這個條件,下面的例子定義了一個Item必須遵循EquatableOtherContainer協定:

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

where 語句

有時候你也可能需要對關聯型別定義更多的限制,這時可以經由在參數列表加上一個where語句,並緊接著限制條件來定義。你可以限制一個關聯型別要遵循某個協定,或是某個型別參數和關聯型別必須相同型別。

底下定義一個泛型函式allItemsMatch(),功能為檢查兩個容器是否包含相同順序的相同元素,如果條件都符合會返回true,否則返回false

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
}

從上述定義可以看到,這個函式的型別參數列表還定義了對兩個型別參數的要求:

  • C1 必須符合協定Container(即C1: Container)。
  • C2 必須符合協定Container(即C2: Container)。
  • C1 的Item必須與 C2 的Item型別相同(即C1.Item == C2.Item)。
  • C1 的Item必須符合協定Equatable(即C1.Item: Equatable)。

接著可以實際使用這個函式,如下:

// 宣告一個型別為 NewStack 的變數 並依序放入三個字串
var newStackOfStrings = NewStack<String>()
newStackOfStrings.push("one")
newStackOfStrings.push("two")
newStackOfStrings.push("three")

// 宣告一個陣列 也放置了三個字串
var arrayOfStrings = ["one", "two", "three"]

// 雖然 NewStack 跟 Array 不是相同型別
// 但先前已將兩者都遵循了協定 Container
// 且都包含相同型別的值
// 所以可以把這兩個容器當做參數傳入函式
if allItemsMatch(newStackOfStrings, arrayOfStrings) {
    print("所有元素都符合")
} else {
    print("不符合")
}
// 印出:所有元素都符合

使用 where 語句的擴展

你也可以在擴展中使用 where 語句,以下是一個為泛型結構Stack增加一個isTop(_:)方法的例子:

extension Stack where Element: Equatable {
    func isTop(_ item: Element) -> Bool {
        guard let topItem = items.last else {
            return false
        }
        return topItem == item
    }
}

這個isTop(_:)方法會先檢查這個 Stack 是不是空的,接著再比對傳入的元素是否與最頂端的元素相同。這個方法使用了==運算子來比對元素,但一開始定義泛型結構 Stack 時並未定義它的元素要遵循Equatable協定,所以必須在擴展中使用 where 語句來增加新的條件,以規範傳入的元素要遵循Equatable協定,也才能正常使用這個新的方法isTop(_:)

如果嘗試在一個元素沒有遵循Equatable協定的 Stack 使用方法isTop(_:),則會發生編譯時錯誤,如下:

// 定義一個空的結構
struct NotEquatable { }

// 宣告一個元素型別為 NotEquatable 的 Stack
var notEquatableStack = Stack<NotEquatable>()

// 宣告一個型別為 NotEquatable 的值 並加入這個 Stack 中
let notEquatableValue = NotEquatable()
notEquatableStack.push(notEquatableValue)

// 因為型別 NotEquatable 沒有遵循 Equatable 協定
notEquatableStack.isTop(notEquatableValue) // 這行會報錯誤

除了遵循協定,也可以限制元素必須為特定的型別,以下的例子為元素的型別必須是Double

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

與先前提到泛型 where 語句一樣,如果這個擴展的 where 語句有多個條件,則是使用逗號,來分隔各條件。

使用 where 語句的關聯型別

你可以在關聯型別後面加上 where 語句來增加條件,如下:

protocol AnotherContainer {
    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
}

泛型下標

下標(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
    }
}

範例

本節範例程式碼放在 ch2/generics.playground

results matching ""

    No results matching ""