泛型的概念
泛型代碼可根據自定義需求,寫出適用于任何類型、靈活且可重用的函數和類型,避免重復的代碼,用一種清晰和抽象的思維表達代碼的意思
泛型函數
示例:
/// 交換變量的值
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)
中T
是Double
類型 -
exchange(&c, &d)
中T
是String
類型
-
注意: 泛型函數在調用的時候, 會根據傳入的值推斷出對應的類型
- 泛型函數格式:
func 函數名<占位類型列表>(參數列表) {
// 函數體
}
- 注意:
- 參數列表中,
占位類型列表
中的占位類型
必須在參數列表
中使用 - 如果
參數列表
中, 多個參數
都屬于相同的占位類型
, 那么這些參數
必需傳入一致的類型數據, 例如: 全部傳入Int
,String
, 或Double
等 - 占位類型列表可以有多個占位類型, 使用逗號分開
- 參數列表中,
類型參數
-
exchange(_:_:)
函數中, 占位類型T
就是一個類型參數的例子, 即:T
是一個類型參數 -
類型參數
指定并命名一個占位類型
, 并且緊隨在函數名后面, 使用一對尖括號括起來(例如:<T>
) - 一旦一個類型參數被指定, 就可以如下使用:
- 用來定義一個函數的參數類型(例如:
exchange(_:_:)
中的one和two的類型) - 做為函數的返回值
- 函數主體中的注釋類型
- 用來定義一個函數的參數類型(例如:
- 類型參數的定義過程中不會代表任何具體的類型, 只是一個占位, 當函數被調用時, 會根據傳入的值的類型, 推斷出具體類型, 例如上面的
Double
和Stirng
替換掉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
的定義中有兩個參數類型
, 分別為Key
和Value
, 而且在給Dictionary
添加元素的時候,key
的值都是唯一的,即Dictionary
根據Key
的值來判斷是修改還是增加元素, 而Swift中的Dictionary
是根據Key
的哈希值來判斷唯一性的, 也就是說Dictionary
的Key
值必須是可哈希的, 所以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> {
// 類的實現部分
}
- 上面的
泛型函數
和泛型類型
都分別有兩個類型參數T
和U
,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
后 使用: Equatable
對T
進行了類型約束。 - 現在可以將該函數使用
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
協議的兩個類型Stack1
和Stack2
, 我們將使用Stack1
和Stack2
的實例對象進行比較根據
allItemsMatch(_:_:)
的作用可以判斷出, 兩個容器Stack1
和Stack2
只有在元素類型(ItemType)類型一致的情況下才能判斷元素是否相等, 但是這個約束使用類型約束
中的方法無法添加, 所以就有了下面的寫法:
// 這里只考慮定義部分, 不考慮實現部分
func allItemsMatch<C1: Container, C2: Container>(_ someContainer: C1, _ anotherContainer: C2) -> Bool where C1.ItemType == C2.ItemType {//函數體}
- 這句代碼中, 使用了
where
語句對C1
和C2
的關聯類型進行了約束, 即C1
和C2
的ItemType
有了這個
where
子句后, 只有Stack1
和Stack2
中的元素類型必須一致才能使用該函數
- 當然僅僅有類型相等判斷是不夠的, 容器中的元素還必須遵守
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 {}