泛型的概念
泛型代码可根据自定义需求,写出适用于任何类型、灵活且可重用的函数和类型,避免重复的代码,用一种清晰和抽象的思维表达代码的意思
泛型函数
示例:
/// 交换变量的值
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 {}