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)
中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 {}