Swift 泛型介绍
为什么会有泛型
下面的 multiNumInt ( _ : _ :)
是个非泛型函数,主要用于计算两个数的乘积
func multiNumInt(_ x: Int, _ y: Int) -> Int {
return x * y
}
multiNum(_:_:)
函数很实用,但是它只能用于 Int
值。如果我们想计算两个 Double
或者其他类型的乘积的值,我们需要在写一些函数,比如 multiNumDouble(_:_:)
函数:
func multiNumDouble(_ x: Double, _ y: Double) -> Double {
return x * y
}
但是我们发现, 函数体是一样的。唯一的区别是它们接收值类型不同 ( Int
、Double
)。
如果是上面这种情况的话,我们在代码书写的时候就会有很多冗余代码,这个时候如果我们想找到一个可以计算任意类型值的函数,那么泛型正是能让我们写出这样函数的语法。
泛型语法注意要点
我们先来看一下泛型的基本写法,首先我我们要指定一个占位符 T
,紧挨着写在函数名后面的 一对尖括号(当前我们这个 T
要遵循 FloatingPoint
协议,计算乘积所必须);其次我们就可以使用 T
来替换任意定义的函数形式参数。
func multiNum<T: FloatingPoint>(_ x: T, _ y: T) -> T {
return x * y
}
下面我们来看一个例子,这是一个栈的数据结构,其中数据结构中的元素类型是 Int
struct LGStack {
private var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int? {
if items.isEmpty { return nil }
return items.removeLast()
}
}
这是一个标准的非泛型版的数据结构,如果我们想要改造一下的话可以像下面这样
struct LGStack<Element> {
private var items = [Element]()
mutating func push(_ item: Element){
items.append(item)
}
mutating func pop() -> Element?{
if items.isEmpty { return nil }
return items.removeLast()
}
}
基本上和栈的数据结构相关都有相同的操作,这个时候我们可以抽取相同的行为定一个协议
protocol StackProtocol {
var itemCount: Int{ get }
mutating func pop() -> Int?
func index(of index: Int) -> Int
}
这里我们定义协议的时候需要指明当前类型,那么能不能给 Protocol
也加上一个泛型呢?
这里我们这样写的话就会出现一个错误,系统提示 Protocol
不支持泛型参数,需要我们使用关联类型来代替。
protocol StackProtocol {
associatedtype Item
var itemCount: Item{ get }
mutating func pop() -> Item?
func index(of index: Int) -> Item
}
关联类型给协议中用到的类型一个占位符名称,协议定义中的相关类型我们都可以用这个占位符
替代,等到真正实现协议的时候在去确定当前占位符的类型,比如下面的代码:
protocol StackProtocol {
associatedtype Item
var itemCount: Int{ get }
mutating func pop() -> Item?
func index(of index: Int) -> Item
}
struct LGStack: StackProtocol {
typealias Item = Int
private var items = [Item]()
var itemCount: Int {
get{
return items.count
}
}
mutating func push(_ item: Item) {
items.append(item)
}
mutating func pop() -> Item? {
if items.isEmpty { return nil }
return items.removeLast()
}
func index(of index: Int) -> Item {
return items[index]
}
}
同样的我们也可以给当前的关联类型添加约束,比如我们要求 Item
必须都要遵循 FixWidthInteger
protocol StackProtocol {
associatedtype Item: FixedWidthInteger
var itemCount: Item{ get }
mutating func pop() -> Item?
func index(of index: Int) -> Item
}
当然我们也可以直接在约束中使用协议
protocol EvenProtocol: StackProtocol {
associatedtype Even: EvenProtocol where Even.Item == Item
func pushEven(_ item: Int) -> Even
}
在这个协议里,Even
是一个关联类型,就像上边例子中 StackProtocol
的 Item
类型一样。 Even
拥有两个约束:它必须遵循 EvenProtocol
协议(就是当前定义的协议),以及它的 Item
类型必须是和容器里的 Item
类型相同。Item
的约束是一个 where
分句。
extension LGStack: EvenProtocol {
func pushEven(_ item: Int) -> LGStack {
var result = LGStack()
if item % 2 == 0 {
result.push(item)
}
return result
}
}
在上面的例子中我们出现了一个 where
分句,泛型 Where
分句要求了关联类型必须遵循指定的协议,或者指定的类型形式参数和关联类型必须相同。泛型 Where
分句以 Where
关键字开 头,后接关联类型的约束或类型和关联类型一致的关系。
func compare<T1: StackProtocol, T2: StackProtocol>(_ stack1: T1, _ stack2:T2) {
guard stack1.itemCount == stack2.itemCount else {
return false
}
for i in 0..<stack1.itemCount {
if stack1.index(of: i) != stack2.index(of: i) {
return false
}
}
return true
}
这个函数有两个形式参数,stack1
和 stack2
。stack1
形式参数是 T1
类型,stack2
形式参 数是 T2
类型。T1
和 T2
是两个容器类型的类型形式参数,它们的类型在调用函数时决定。
下面是函数的两个类型形式参数上设置的要求:
-
T1
必须遵循StackProtocol
协议(写作T1: StackProtocol
); -
T2
也必须遵循StackProtocol
协议(写作T2: StackProtocol
); -
T1
的Item
必须和T2
的Item
相同(写作T1.Item == T2.Item
); -
T1
的Item
必须遵循Equatable
协议(写作T1.ItemType: Equatable
)。
前两个要求定义在了函数的类型形式参数列表里,后两个要求定义在了函数的泛型 Where
分句中。
这些要求意味着:
-
stack1
是一个T1
类型的容器 -
stack2
是一个T2
类型的容器; -
stack1
和stack2
中的元素类型相同
类型擦除
前面我们已经了解了什么是协议,什么是泛型。接下来我们了解一下什么是类型擦除。首先我们先来看一个例子(这里我们定义了一个泛型协议)
protocol DataFetch {
associatedtype dataType
func fetch(completion: ((Result<DataType, Error>) -> Void)?)
}
紧接着我们需要请求一个用户数据
struct User {
let userId: Int
let name: String
}
struct UserData: DataFetch {
typealias dataType = User
func fetch(completion: ((Result<FetchType, Error>) -> Void)?) {
let user = User(userId: 1001, name: "Kody")
completion?(.success(user))
}
}
这时候我们需要使用我们当前的 UserData
获取我们当前的 User
数据
class someViewController {
let userData: DataFetch
}
运行一下,会发现一个报错信息
也就意味着 DataFetch
只能用作泛型约束,不能用作具体类型!因为编译器无法确定
dataType
的具体类型是什么!那有同学就会说了,这还不简单我直接改成下面这样就行了。
但是这会在 ViewController
和 UserData
对象之间创建一个依赖关系。如果我们遵循 SOLID
原则,我们希望避免依赖并隐藏实现细节。这个时候怎么办?我们就需要引入一个中间层来解决 这样的问题。
struct AnyDataFetch<T>: DataFetch {
typealias DataType = T
private let _fetch: (((Result<T, Error>) -> Void)?) -> Void
init<U: DataFetch>(_ fetchable: U) where U.DataType == T {
_fetch = fetchable.fetch
}
func fetch(completion: ((Result<T, Error>) -> Void)?) {
_fetch(completion)
}
}
- 这里我们定义了一个中间层结构体
AnyDataFetch
,AnyDataFetch
实现了DataFetch
的所 有方法。 - 在
AnyDataFetch
的初始化过程中,实现协议的类型会被当做参数传入(依赖注入) - 在
AnyDataFetch
实现的具体协议方法fetch
中,再转发实现协议的抽象类型。
这个时候我们就可以把 AnyDataFetch
当做具体类型使用。
class someViewController {
let userData: AnyDataFetch<User>
init(userData: AnyDataFetch<User>) {
self.userData = userData
}
}
let userData = UserData()
let anyDataFetch = AnyDataFetch<User>(userData)
vc.userData.fetch { (result) in
switch result {
case .success(let user):
print(user.name)
case .failure(let error):
print(error)
}
}
这里可能大家看不出有什么区别,这样做的好处就是我们不用知道当前请求的具体类型是什么,
也就意味着如果我改变了之后
struct VIPFetch: DataFetch {
typealias DataType = User
func fetch(completion: ((Result<DataType, Error>) -> Void)?) {
let user = User(id: 0001, name: "VIP")
completion?(.success(user))
}
}
let vipFetch = VIPFetch()
let anyDataFetch = AnyFetchable<User>(vipFetch)
let vc = someViewController.init(userData: anyDataFetch)
someDaveStruct.userFetch.fetch { (result) in
switch result {
case .success(let user):
print(user.name)
case .failure(let error):
print(error)
}
}
对于 someViewController
, 我接收的其实还是 AnyFetchable<User>
类型,这其实就是所谓的类 型擦除,系统中的 AnySequence
、AnyCollection
都是这样的原理。
AnySequence 使用案例补充
假设你有这样一个需求,你需要迭代你的自定义属性 User
,其中 User
属性如下
struct User {
var userId: Int
var name: String
}
此刻对于 User
来说,我们应该要做的是实现 Sequence
协议
struct User: Sequence {
var userId: Int
var name: String
func makeIterator() -> CustomIterator {
return CustomIterator(obj: self)
}
}
struct CustomIterator: IteratorProtocol {
var children: Mirror.Children
init(obj: Persion) {
children = Mirror(reflecting: obj).children
}
mutating func next() -> String? {
guard let child = children.popFirst() else { return nil }
return "\(child.label.wrapped) is \(child.value)"
}
}
那这个时候,对于我们当前的另一个⻚面的 VIP
用户数据,也需要遍历,其中 VIP
的数据类型如下。
struct VIP {
var vipdate: String
var viplevel: Int
var vipName: String
}
对于 VIP
和 USer
来说他们的行为是一致的,所以这里我们希望抽象出一个统一的协议
struct CustomDataIterator: IteratorProtocol {
var children: Mirror.Children
init(obj: Any) {
children = Mirror(reflecting: obj).children
}
mutating func next() -> String? {
guard let child = children.popFirst() else { return nil }
return "\(child.label.wrapped) is \(child.value)"
}
}
protocol CustomDataSeuqence: Sequence { }
extension CustomDataSeuqence {
func makeIterator() -> CustomDataIterator {
return CustomDataIterator(obj: self)
}
}
此刻对于我们的 VIP
来说,只需要这样做
struct VIP: CustomDataSeuqence {
var vipdate: String = "20221230"
var viplevel: Int = 10
var vipName: String = "lg"
}
接下来比如我们要做一个社群功能,需要当前同一个社群当中的用户进行遍历,如果是 VIP
,需 要特殊显示一些效果
for obj in Users {
for userProperty in obj {
print(userProperty)
}
}
那么这里的 Users
应该定义成什么类型呢?定义成 Any
类型,这个时候对于当前页面来说就需要将 Any
的类型转成 CustomDataSeuqence
,这样虽然可以,但是有点牵强,因为我们已经明确知道当前的类型
let users: [CustomDataSeuqence]
如果是这样传值,编译器肯定会报错,因为当前编译器无法确定类型,这个时候我们就可以使用 AnySeuqence
let user = User()
let vip = VIP()
let users: [AnySequence<String>] = [AnySequence(user), AnySequence(vip)]
for user in users {
// print(user)
for item in user {
print(item)
}
}
这个时候 AnySequence
就将具体的 Sequence
类型隐藏了,调用者只知道数组中的元素是一个可以迭代输出字符串类型的序列。
Sequence & Collection
Sequence
协议来说,表达的是既可以是一个有限的集合,也可以是一个无限的集合,而它只需 要提供集合中的元素,和如何访问这些元素的接口即可。
Iterators
在我们研究 Sequence
之前,我们先从我们日常比较常⻅的一段代码入手:
let numbers = [2, 4, 6, 8]
for num in numbers {
print(num)
}
这就是一个非常常⻅的 for
循环,但是大家有没有思考过,这里是如何进行 for
循环的遍历的?这里我们直接编译一下源码,借助 SIL
来看一下代码的执行流程
这里的代码我们就不用分析基本上一看就是在分配内存空间,然后把字面量的值存入到 Array
当中
创建了一个 name
为 $num$generator
的 IndexingIterator
可以看到这里调用了一个 makeIterator()
的方法,这个方法需要两个参数一个是在上文中创建的 IndexingIterator
, 一个是 Array
的引用。我们接着往下看
这里调用 IndexingIterator
的 next()
方法来遍历数组中一个又一个的元素。
接下来我们定位到 Collection.swift
这个文件,我们看一下:
我们来找到这个文件,是一个一次提供一个序列值的类型, 它和协议是息息相关的,每次通过创建迭代器来访问序列中的元素。
所以我们每次在使用 for..in
的时候,其实都是使用这个集合的迭代器来遍历当前集合或者序列当中的元素。
我们来看一下 IteratorProtocol
的定义:
当前协议有一个关联类型 Element
,其实有一个 mutating
的方法 next
,next
方法返回一 个 element
。
接下来就是 Sequence
协议的定义:
对于 Sequence
协议来说,表达的是既可以是一个有限的集合,也可以是一个无限的集合,而它 只需要提供集合中的元素,和如何访问这些元素的接口即可。
我们来自定义的试一下:假设我们要用一个结构体来模拟一个集合,对于一个给定的初始值,那么当前集合中包含从 0...count
的整形集合。
同样的我们也可以创建一个无限的序列,下面这个代码就会打印无穷的数字(除非你在开始给定的就是 nil
)
struct unlimitedIterator: IteratorProtocol {
let value: Int
func next() -> Int? {
return value
}
}
var iterator = unlimitedIterator(value: 10)
while let x = iterator.next() {
print(x)
}
Collection协议
定义 startIndex
和 endIndex
属性,表示集合起始和结束位置;
定义一个只读的下标操作符;
实现一个 index(after:)
方法用于在集合中移动索引位置;
MutableCollection
: 允许集合通过下标修改自身元素
MutableCollection
支持集合通过下标的方式改变自身的元素,比如上个案例当中我们实
现了 subscript
的 get
和 set
,对于 Collection
来说可以不提供 set
,这个时候我们就没有办法通过下标的方式改变自身元素了,所以对 MutableColletion
来说下标语法提供一个
setter
就行
RangeReplaceColletion
: 允许集合修改任意区间的元素
那这里如果我们想 Remove
掉一个元素是不是需要自己实现 Remove
方法呢?这些在 Swift
标准库中有专⻔的协议约定了,这个协议叫做 RangeReplaceColletion
。
这个协议允许我们通过一个集合来替换当前集合当中任意自己的元素,同时支持我们删除和插入
元素的操作。
当我们自定义集合要遵循 RangeReplaceableCollection
的时候,我们需要提供一个默认的 init
方法,以及 replaceSubrange(:with:)
方法。其他的如果不需要 RangeReplaceableCollection
都有默认的实现。
BidirectionalCollection
:可以向前或向后遍历集合
比如可以获取最后一个元素、反转序列、快速的获取倒序等等。既然正序是通过 subscript
和
index(after:)
来实现的,那么倒序添加一个 index(before:)
就可以往前递归了,这就好像双向链表一样,只不过双向链表获取的是值,而这里的集合获取的都是索引。
RandomAccessCollection
:任意访问集合元
实现 index(:offsetBy:)
和 distance(from:to:)