Swift: 泛型

泛型的概念

泛型代碼可根據自定義需求,寫出適用于任何類型、靈活且可重用的函數和類型,避免重復的代碼,用一種清晰和抽象的思維表達代碼的意思

泛型函數

示例:

/// 交換變量的值
func exchange<T>(_ one: inout T, _ two: inout T) {
    (one, two) = (two, one)
}

var a = 10.0
var b = 20.0

print("a = \(a), b = \(b)")     // a = 10.0, b = 20.0
exchange(&a, &b)               // 交換a和b的值
print("a = \(a), b = \(b)")     // a = 20.0, b = 10.0

var c = "hellow"
var d = "world"

print("a = \(c), b = \(d)")     // a = hellow, b = world
exchange(&c, &d)               // 交換c和d的值
print("a = \(c), b = \(d)")     // a = world, b = hellow
  • 上述代碼中, exchange(_:_:)函數就是一個泛型函數

  • <T>中的<T>是一個占位類型, 在定義過程中不確定具體的類型, 只有在函數調用時, 根據傳入的值的類型, 來推斷出T的具體類型

    • exchange(&a, &b)TDouble類型
    • exchange(&c, &d)TString類型

注意: 泛型函數在調用的時候, 會根據傳入的值推斷出對應的類型

  • 泛型函數格式:
func 函數名<占位類型列表>(參數列表) {
    // 函數體
}
  • 注意:
    • 參數列表中, 占位類型列表中的占位類型必須在參數列表中使用
    • 如果參數列表中, 多個參數都屬于相同的占位類型, 那么這些參數必需傳入一致的類型數據, 例如: 全部傳入Int, String, 或Double
    • 占位類型列表可以有多個占位類型, 使用逗號分開

類型參數

  • exchange(_:_:)函數中, 占位類型T就是一個類型參數的例子, 即: T是一個類型參數
  • 類型參數指定并命名一個占位類型, 并且緊隨在函數名后面, 使用一對尖括號括起來(例如: <T>)
  • 一旦一個類型參數被指定, 就可以如下使用:
    • 用來定義一個函數的參數類型(例如: exchange(_:_:)中的one和two的類型)
    • 做為函數的返回值
    • 函數主體中的注釋類型
  • 類型參數的定義過程中不會代表任何具體的類型, 只是一個占位, 當函數被調用時, 會根據傳入的值的類型, 推斷出具體類型, 例如上面的DoubleStirng替換掉T
  • 參數類型可以同時存在多個, 并用逗號分開, 例如: <T, U, S>為三個類型參數(占位類型), 名稱分別為參數類型T, 參數類型U, 參數類型S
綜上有泛型函數格式如下:
func 函數名<類型參數列表>(參數列表) {
   // 函數體
}

泛型類型

  • 泛型函數是在函數名的后面緊跟著類型參數列表, 而泛型類型就是在定義的類型的時候, 在類型名后面緊跟類型參數列表
示例
  • 泛型類:
泛型類: 
class GenericClass<Element> {
    // 集合
    var items = [Element]()
    // 壓棧
    func push(_ item: Element) {
        items.append(item)
    }
    // 出棧
    func pop() -> Element? {
        return items.isEmpty ? nil : items.removeLast()
    }
}
  • 泛型結構體
泛型結構體:
struct GenericStruct<Element> {
    // 集合
    var items = [Element]()
    // 壓棧
    mutating func push(_ item: Element) {
        items.append(item)
    }
    // 出棧
    mutating func pop() -> Element? {
        return items.isEmpty ? nil : items.removeLast()
    }
}
  • 泛型枚舉:
泛型枚舉:
enum GenericEnum<Element> {
    case none
    case some(Element)
}
  • 上面的 GenericClass(類), GenericStruct(結構體), GenericEnum(枚舉)都是泛型類型, 在類型名后緊跟著泛型的類型參數 <Element>
  • 泛型類型使用的時候, 需要指定類型參數的具體類型, 下面以結構體GenericStruct為例:
// 創建GenericStruct類型的機構體變量struct
// 指定類型參數為 Int
var struct = GenericStruct<Int>()
// 使用struct時, push(_:), pop()方法使用 類型參數的地方 都會替換為Int類型
struct.push(1)
struct.push(2)
struct.push(3)
struct.push("4")   // 報錯: 因為push(_:)接收的參數類型已經被替換成Int

let result = struct.pop()   // result = 3
給泛型類型添加分類(extension)
  • 泛型類型添加分類時, 定義中不可以增加新的類型參數, 也不需要寫已有類型參數(編譯器也不允許寫)
  • 錯誤寫法:
錯誤一: 分類的定義中不可以增加新的類型參數
extension GenericClass<T> { }
錯誤二: 分類的定義中不需要寫已經有的類型參數
extension GenericClass<Element> { }
  • 下面的代碼是正確寫法, 在分類中可以使用類型定義時有的類型參數:
extension GenericClass {
    // 使用已有的 類型參數: Element 做為返回值
    func element(at index: Int) -> Element? {
        if index < items.count {
            return items[index]
        }else {
            return nil
        }
    }
}

雖然在分類中無法定義新的類型參數, 但是可以在分類新定義的方法中引入其他的類型參數

extension GenericClass {
   func exchange<T>(one: inout T, two: inout T) {
       (one, two) = (two, one)
   }
}

泛型約束

  • 上述所有代碼中, 不論是泛型函數中的 類型參數T , 還是泛型類型中的 類型參數Element , 都可以在調用時指定任意一個具體的類型做為替換, 這是因為我并沒有給這些參數類型添加任何的約束
那么什么是 參數類型添加約束呢?

就拿我們經常使用的Dictionary為例, 我們知道Dictionary的定義中有兩個參數類型, 分別為 KeyValue , 而且在給Dictionary添加元素的時候, key的值都是唯一的,即Dictionary根據Key的值來判斷是修改還是增加元素, 而Swift中的Dictionary是根據Key的哈希值來判斷唯一性的, 也就是說DictionaryKey值必須是可哈希的, 所以Dictionary的類型參數Key有一個約束, 那就是 可哈希的值

  • 下面是Dictionary定義的代碼部分, 這里過濾里面的實現部分, 其中的where會在后面講解
public struct Dictionary<Key, Value> : Collection, ExpressibleByDictionaryLiteral where Key : Hashable{}
類型約束語法
  • 在定義一個類型參數時, 在類型參數后面放置一個類名或者協議名, 并用冒號分開, 來定義類型參數類型約束, 他們將成為類型參數列表的一部分
  • 示例:
泛型函數添加類型約束
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
    // 這里是反省函數的函數體部分
}

泛型類型添加類型約束
class GenericClass<T: SomeClass, U: SomeProtocol> {
    // 類的實現部分
}
  • 上面的 泛型函數泛型類型 都分別有兩個類型參數 TU, T有一個類型約束: 必須是SomeClass類的子類; U有一個類型約束: 必須遵守SomeProtocol協議的類型
類型約束實踐
  • 現在有一個非泛型函數findIndex(ofString:in:), 該函數的功能是在一個String數組中查找給定的String值的索引, 若找到匹配的String值, 會返回該String值在String數組中的索引, 否則返回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:)函數可以用于查找字符串數組中某個字符串的索引值
let strings = ["a", "b", "c", "d", "e"]
if let foundIndex = findIndex(of: "c", in: strings) {
    print("c 的索引值是 \(foundIndex)")
}
// 打印: c 的索引值是 2
  • 我們知道, findIndex(ofString:in:)函數目前只能查找字符串在數組中的索引值, 用處不是很大。不過, 我們可以用占位類型T替換掉String類型來寫出具有相同功能的泛型函數findIndex(of:in:)
  • 下面就是使用占位類型T替換掉String類型的代碼:
func findIndex<T>(of valueToFind: T, in array: [T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}
  • 這段代碼看上去可以查找任意的具體類型在該類型數組中的索引值, 但是在Xcode中并不能正常運行

  • 這是因為在Swift中, 想要比較兩個值是否相等, 那么這兩個值的類型必須實現了Equatable協議才可以

  • 對于未實現Equatable協議的類型, 比如我們自定義的類和結構體的實例, 是不能直接使用 == 來比較的, 因為這些實例并不知道"相等"意味著什么, 是部分內容相等才相等, 還是完全相等才算相等, 而Equatable就是用來說明"相等"意味著什么的

  • 因為只有遵守Equatable協議的類型才能進行相等判斷, 所以上述可以被替換成為任意類型的T就不能符合要求, 所以我們需要給T加上一個類型約束: 想要替換占位類型T的具體類型, 必須遵守Equatable協議

  • 任何遵守Equatable協議的類型都可以在findIndex(of:in:)中正常運行, 代碼如下:

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

關聯類型(泛型協議)

  • 類、結構體和枚舉的泛型類型中, 將類型參數列表放在了類型名的后面, 而在泛型協議中卻不能這樣寫

  • 泛型協議的寫法與泛型類型有所不同, 需要使用 associatedtype 關鍵字來指定類型參數

  • 泛型協議中的類型參數又被稱為關聯類型, 其代表的實際類型在協議被采納時才會被指定

  • 下面一段代碼就是有關聯類型的協議:

protocol Container {
    associatedtype ItemType
    mutating func append(item: ItemType)
    var count: Int { get }
    subscript(i: Int) -> ItemType { get }
}
  • 協議Container定義了三個任何采納該協議的類型必須提供的功能

    • 必須可以通過append(_:)方法添加一個類型為ItemType的新元素到容器里

    • 必須可以通過count屬性獲取容器中元素的數量, 并返回一個Int值

    • 必須可以通過索引值類型為Int的下標檢索到容器中的每一個類型為ItemType的元素

  • 協議中無法定義ItemType的具體類型, 而任何遵從Container協議的類型都必須指定關聯類型 ItemType 的具體類型

  • 下面的是一個非泛型的IntStack結構體, 采納并符合了Container協議, 實現了Container協議的是三個要求:

struct IntStack: Container {
    // 集合數組, 用于存放元素
    var items = [Int]()
  
    // Container協議部分
    typealias ItemType = Int  // 通過關鍵字 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在實現Container的要求時, 指定了ItemType的類型為Int, 即typealias ItemType = Int, 從而將Container協議中抽象的ItemType類型轉換為具體的Int類型

  • 由于Swift的類型推斷, 實際上不用再IntStack中特意的聲明ItemType的類型為Int也可以, 這是因為IntStack符合Container協議的所有要求, 并且在方法中也將ItemType寫成了Int類型, 這樣Swift就可以推斷出ItemType的類型為Int, 事實上, 在代碼中刪除typeealias IntType = Int這一行, 一切依舊可以正常工作

struct IntStack: Container {

    var items = [Int]()

    mutating func append(item: Int) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Int {
        return items[i]
    }
}
  • 上面的代碼自動推斷出 IntType 的類型是 Int

即: 對于泛型協議, 當有類型遵從該協議的時候, 只需要給未確定具體類型的關聯類型所參與的所有方法中, 都給出唯一指定類型時, 并不需要特意聲明該關聯類型的聲明也能正常運行, 原因就是Swift的自動推斷

  • 也可以讓泛型Stack結構體遵從Container協議
struct Stack<Element>: Container {
    var items = [Element]()
    // 由于所有需要關聯類型的地方都指定了明確類型, 就不需要在特意的聲明關聯類型具體是什么類型了, 這里自動推斷出 ItemType的類型是Stack對象創建時指定的泛型具體類型
    // typealias ItemType = Element
    mutating func append(item: Element) {
        self.items.append(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Element {
        return items[i]
    }
}
通過擴展一個存在的類型來指定關聯類型
  • 對于一個已經存在的類型, 并且在定義中沒有遵守泛型協議, 我們可以在它的extension中遵守需要的泛型協議, 并且在該擴展中 也可以自動推導ItemType的類型, 并不需要寫typealias ItemType = Element
struct Stack<Element> {
    var items = [Element]()
}
// 通過擴展遵從泛型協議 Container
extension Stack: Container {
    // 這一行可以不寫
    // typealias ItemType = Element

    mutating func append(item: Element) {
        self.items.append(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Element {
        return items[i]
    }
}

泛型 Where 語句

  • 上面的敘述中, 類型約束讓我們能夠為泛型函數泛型類型類型參數定義一些強制要求
  • 除了類型約束以外, 還有一種方法給泛型函數泛型類型類型參數定義約束, 那就是where子句
  • 現有如下方法:
func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}
  • 該函數就是類型約束中使用的, 獲取一個元素, 在該元素數組中的索引值的函數, 在占位類型T后 使用: EquatableT進行了類型約束。
  • 現在可以將該函數使用where子句變形為下面代碼:
func findIndex<T>(of valueToFind: T, in array: [T]) -> Int? where T : Equatable {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}
  • 變形后的代碼, 將類型約束提取出來, 放在了返回值的后面, 使用where子句來表達該約束, 這在語法上沒有任何問題, 并且變形后的函數依然可以正常使用

  • 當然, 如果僅僅是給類型參數添加類型約束, 僅僅需要第一種方式就可以了。 實際上where子句還有另外一個用法, 即: where子句除了給類型參數添加類型約束外, 還可以給關聯類型添加約束

  • 通過下面的代碼示例進行講解where子句給關聯類型添加約束的用法
// 容器協議
protocol Container {
    associatedtype ItemType
    
    mutating func append(item: ItemType)
}

func allItemsMatch<C1: Container, C2: Container>(_ someContainer: C1, _ anotherContainer: C2) -> Bool {
    // 檢查兩個容器包含相同數量的元素
    if someContainer.count != anotherContainer.count {
        return false
    }
    // 檢查每個元素是否相等
    for i in 0..<someContainer.count {
        if someContainer[i] != anotherContainer[i] {
            return false
        }
    }
    return true
}
  • 上述代碼中, allItemsMatch(_:_:)函數是一個泛型函數, 作用是判斷兩個都遵守了Container協議的容器C1, C2中所有的元素是否在位置和值上完全相等
  • 由于C1和C2是兩個不同的占位類型, 所以C1和C2可以是兩個不同的類型
// 遵守Container協議的類型Stack1
class Stack1<Element>: Container {
    var items = [ItemType]()
    
    typealias ItemType = Element
    
    func append(item: Element) {
        items.append(item)
    }
}

// 遵守Container協議的類型Stack2
class Stack2<Element>: Container {
    var items = [ItemType]()
    
    typealias ItemType = Element
    
    func append(item: Element) {
        items.append(item)
    }
}
  • 上面定義了都遵守Container協議的兩個類型Stack1Stack2, 我們將使用Stack1Stack2的實例對象進行比較

  • 根據allItemsMatch(_:_:)的作用可以判斷出, 兩個容器Stack1Stack2只有在元素類型(ItemType)類型一致的情況下才能判斷元素是否相等, 但是這個約束使用類型約束中的方法無法添加, 所以就有了下面的寫法:

// 這里只考慮定義部分, 不考慮實現部分
func allItemsMatch<C1: Container, C2: Container>(_ someContainer: C1, _ anotherContainer: C2) -> Bool where C1.ItemType == C2.ItemType {//函數體}
  • 這句代碼中, 使用了where語句對C1C2的關聯類型進行了約束, 即C1C2ItemType

有了這個where子句后, 只有Stack1Stack2中的元素類型必須一致才能使用該函數

  • 當然僅僅有類型相等判斷是不夠的, 容器中的元素還必須遵守Equatable才行, 即C1.ItemType == C2.ItemType并且C1.ItemType: Equatable, 所以allItemsMatch(_:_:)函數的完整代碼如下:
func allItemsMatch<C1: Container, C2: Container>(_ someContainer: C1, _ anotherContainer: C2) -> Bool where C1.ItemType == C2.ItemType, C1.ItemType: Equatable {
    // 檢查兩個容器包含相同數量的元素
    if someContainer.count != anotherContainer.count {
        return false
    }
    // 檢查每個元素是否相等
    for i in 0..<someContainer.count {
        if someContainer[i] != anotherContainer[i] {
            return false
        }
    }
    return true
}
  • 在where子句中, 使用逗號分隔多個約束

類型的定義中也可以使用where子句添加約束, 用法與在泛型函數中一樣, 都寫在定義的后面, 大括號{}的前面

  • 示例:
class Stack<Element>: Container where Stack.ItemType : Equatable {
    var items = [ItemType]()
    
    typealias ItemType = Element
    
    func append(_ item: Element) {
        items.append(item)
    }
}
具有泛型where子句的擴展
  • 你可以使用泛型where子句做為擴展的一部分, 基于以前的例子, 下面的示例擴展了泛型Stack結構體, 添加一個isTop:方法
extension Stack where Element: Equatable {
    func isTop(_ item: Element) -> Bool {
        return items.last == item
    }
}
  • 以下是 isTop(_:) 方法的調用方式:
if stackOfStrings.isTop("c") {
    print("Top element is c.")
} else {
    print("Top element is something else.")
}
// 打印 "Top element is c."
  • 如果嘗試在包含的元素不符合Equatable協議的棧上調用isTop(_:)方法, 則會收到編譯時錯誤
struct NotEquatable { }
var notEquatableStack = Stack<NotEquatable>()
let notEquatableValue = NotEquatable()
notEquatableStack.push(notEquatableValue)
notEquatableStack.isTop(notEquatableValue)  // 報錯
  • 你可以使用where子句擴展一個協議, 基于以前的實例, 下面的實例擴展了Container協議, 添加一個startsWith(:_)方法
extension Container where ItemType: Equatable {
    func startWith(_ item: ItemType) -> Bool {
        return count >= 1 && self[0] == item
    }
}

extension Array: Container{}

let array = ["a", "b", "c"]

if array.startWith("a") {
    print("array 第一個元素是 a")
}else {
    print("array 第一個元素不是 a")
}
// 打印 array 第一個元素是 a
  • 除了給泛型類型泛型協議分類中添加上述的where子句外, 還可以直接約束ItemType的具體類型:
extension Container where ItemType == Double {
    func average() -> Double {
        var sum = 0.0
        for i in 0..<count {
            sum += self[i]
        }
        return sum / Double(count)
    }
}
print([1260.0, 1200.0, 98.6, 37.0].average())
// 打印 "648.9"
  • 只有容器中的元素為Double類型時, 才可以調用該方法
具有泛型 Where 子句的關聯類型
  • 除了上述使用方法外, where子句還可以直接在泛型協議的定義中, 直接給泛型協議參數類型添加約束
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
}
  • 上述代碼Container協議中, 通過associatedtype Iterator定義了一個類型參數Iterator, 并使用類型約束, 使Iterator必須遵從IteratorProtocol協議, 又使用where子句, 約束了Iterator對應IteratorProtocol協議的類型參數的類型必須和associatedtype Item類型一致

  • 一個協議繼承了另一個協議, 你通過在協議聲明的時候, 包含泛型Where子句, 來添加一個約束到被繼承協議的關聯類型, 例如, 下面的代碼聲明了一個ComparableContainer協議, 他要求所有的Item必須是Comparable的

protocol ComparableContainer: Container where Item: Comparable {}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 本章將會介紹 泛型所解決的問題泛型函數類型參數命名類型參數泛型類型擴展一個泛型類型類型約束關聯類型泛型 Where...
    寒橋閱讀 649評論 0 2
  • 136.泛型 泛型代碼讓你可以寫出靈活,可重用的函數和類型,它們可以使用任何類型,受你定義的需求的約束。你可以寫出...
    無灃閱讀 1,522評論 0 4
  • 泛型代碼可以確保你寫出靈活的,可重用的函數和定義出任何你所確定好的需求的類型。你的可以寫出避免重復的代碼,并且用一...
    iOS_Developer閱讀 813評論 0 0
  • 泛型(Generics) 泛型代碼允許你定義適用于任何類型的,符合你設置的要求的,靈活且可重用的 函數和類型。泛型...
    果啤閱讀 693評論 0 0
  • 說來慚愧,從五六歲開始讀書識字至今,時間不可謂不短。但拋開各類考證考級的被動性讀書,自己總感覺從讀書中受益了...
    多能干細胞閱讀 185評論 0 1