Swift进阶-泛型
Swift进阶-类与结构体
Swift-函数派发
Swift进阶-属性
Swift进阶-指针
Swift进阶-内存管理
Swift进阶-TargetClassMetadata和TargetStructMetadata数据结构源码分析
Swift进阶-Mirror解析
Swift进阶-闭包
Swift进阶-协议
Swift进阶-泛型
Swift进阶-String源码解析
Swift进阶-Array源码解析
为什么会有泛型?
func multiNumInt(_ x: Int, _ y: Int) -> Int {
return x * y
}
func multiNumDouble(_ x: Double, _ y: Double) -> Double {
return x * y
}
我们发现, multiNumInt(::) 、 multiNumDouble(::) 函数体是一样的。唯一的区别是它们接收值类型不同( Int 、 Double )。
这个时候我们想找到一个可以计算任意类型值的函数怎么办?泛型正是能让我们写出这样函数的语法。
一、泛型语法
首先我我们要指定一个占位符 T
,紧挨着写在函数名后面的 一对尖括号(当前我们这个 T
要遵循 FloatingPoint
协议,计算乘积所必须);其次我们就可以使用 T
来替换任意定义的函数形式参数。
- 举例一:函数的泛型
func multiNum<T: FloatingPoint>(_ x: T, _ y: T) -> T {
return x * y
}
- 举例二:类型的泛型
struct Stack {
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 Stack<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 Stack: 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
拥有两个约束:
1.它必须遵循 EvenProtocol
协议(就是当前定义的协议);
2.以及它的 Item
类型必须是和容器里的 Item
类型相同。
Item
的约束是一个 where
分句。
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
}
struct Stack: 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]
}
}
extension Stack: EvenProtocol {
func pushEven(_ item: Int) -> Stack {
var result = Stack()
if item % 2 == 0 {
result.push(item)
}
return result
}
}
在上面的例子中我们出现了一个where
分句,泛型 Where
分句要求了关联类型必须遵循指定的协议,或者指定的类型形式参数和关联类型必须相同。
泛型 Where
分句以 Where
关键字开头,后接关联类型的约束或类型和关联类型一致的关系。
- 案例六:函数泛型约束
protocol StackProtocol {
associatedtype Item: FixedWidthInteger
var itemCount: Item { get }
mutating func pop() -> Item?
func index(of index: Int) -> Item
}
func compare<T1: StackProtocol, T2: StackProtocol>(_ stack1: T1, _ stack2: T2) -> Bool {
guard stack1.itemCount == stack2.itemCount else { return false }
for i in 0..<stack1.itemCount {
if stack1.index(of: Int(i)) != stack2.index(of: Int(i)) {
return false
}
}
return true
}
这个函数有两个形式参数, stack1
和 stack2
。 stack1
形式参数是 T1
类型, stack2
形式参数是 T2
类型。 T1
和 T2
是两个容器类型的类型形式参数,它们的类型在调用函数时决定。
下面是函数的两个类型形式参数上设置的要求:
1、 T1 必须遵循 StackProtocol 协议(写作 T1: StackProtocol );
2、 T2 也必须遵循 StackProtocol 协议(写作 C2: StackProtocol );
3、 T1 的 Item 必须和 T2 的 Item 相同(写作 T1.Item == C2.Item);
4、 T1 的 Item 必须遵循 Equatable 协议(写作 T1.ItemType: Equatable )。
前两个要求定义在了函数的类型形式参数列表里,后两个要求定义在了函数的泛型 Where
分句中。
这些要求意味着:
1、 stack1 是一个 T1 类型的容器;
2、 stack2 是一个 T2 类型的容器;
3、 stack1 和 stack2 中的元素类型相同
二、类型擦除
给一个类型耦合的例子:
// 定义数据拉取协议
protocol DataFetch {
associatedtype DataType
func fetch(completion: ((Result<DataType, Error>)->Void))
}
// 数据模型
struct User {
let userId: Int
let name: String
}
// 定义数据拉取工具类 - 并且确定拉取后通过闭包返回User模型
struct UserDataFetch: DataFetch {
typealias DataType = User
// 类似网路网络请求,获取数据
func fetch(completion: ((Result<DataType, Error>) -> Void)) {
let user = User(userId: 1001, name: "安安")
completion(.success(user))
}
}
接下来看看ViewController会耦合的地方:
class MyViewController {
let dataFetch: UserDataFetch
init(_ dataFetch: UserDataFetch) {
self.dataFetch = dataFetch
}
}
可以看到ViewController
里面声明了一个UserDataFetch
实例,那就会出现一种情况是那如果我又有一个VipUserDataFetch
类型呢,那就又得嵌套进了ViewController
了,那就会显得臃肿且耦合,那如果我们声明成DataFetch
协议类型呢?
因为我们声明是一个泛型协议,会报错的:
想要解决上面的耦合性,又想VC传入的时候使用泛型协议且又想绕过编译器的检查,可以抽象出一层中间层AnyDataFetch
通过依赖注入的方式把协议的具体类型传递进来了,并让其做一个泛型约束:
- 这里我们定义了一个中间层结构体
AnyDataFetch
,AnyDataFetch
实现了DataFetch
的所有方法。 - 在
AnyDataFetch
的初始化过程中,实现协议的类型会被当做参数传入(依赖注入) - 在
AnyDataFetch
实现的具体协议方法fetch
中,再转发实现协议的抽象类型。
// 定义一个中间层处理vc数据接收,因为vc接收的不一定只有 UserData 获取的数据,也可以VipUserData等等...
struct AnyDataFetch<T>: DataFetch {
typealias DataType = T
private let _fetch: ((Result<T, Error>) -> Void) -> Void
init<U: DataFetch>(_ fetchable: U) where U.DataType == T {
self._fetch = fetchable.fetch
}
func fetch(completion: ((Result<T, Error>) -> Void)) {
_fetch(completion)
}
}
class MyViewController {
let dataFetch: AnyDataFetch<User>
init(_ dataFetch: AnyDataFetch<User>) {
self.dataFetch = dataFetch
}
}
let userData = UserDataFetch()
let anyDataFetch = AnyDataFetch<User>(userData)
let myVC = MyViewController(anyDataFetch)
myVC.dataFetch.fetch { result in
switch result {
case .success(let user):
print(user.name)
case .failure(let error):
print(error)
}
}
这里可能大家看不出有什么区别,这样做的好处就是我们不用知道当前请求的具体类型是什么, 也就意味着如果我改变了之后,我还有一个VipDataFetch
遵循了DataFetch
协议:
struct VipDataFetch: DataFetch {
typealias DataType = User
func fetch(completion: ((Result<DataType, Error>) -> Void)) {
let user = User(userId: 0001, name: "VIP")
completion(.success(user))
}
}
let vipDataFetch = VipDataFetch()
let anyDataFetch1 = AnyDataFetch<User>(vipDataFetch)
let vc = MyViewController(anyDataFetch1)
vc.dataFetch.fetch { (result) in
switch result {
case .success(let user):
print(user.name)
case .failure(let error):
print(error)
}
}
对于VC来说压根儿不关心具体的DataFetch类型
了,关注点只放在model上。
系统上对于类型擦除的运用比如:AnyCollection、AnySequence
AnySequence使用案例
假设你有这样一个需求,你需要迭代你的自定义属性 User
;此刻对于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: Any) {
children = Mirror(reflecting: obj).children
}
mutating func next() -> String? {
guard let child = children.popFirst() else { return nil }
return "\(child.label ?? "无label") is \(child.value)"
}
}
那这个时候,对于我们当前的另一个页面的 VIP
用户数据,也需要遍历,对于 VIP
和 USer
来说他们的行为是一致的,所以这里我们希望抽象出一个统一的协议:
struct VIP: CustomDataSeuqence{
var vipdate: String = "20221226"
var viplevel: Int = 18
var vipName: String = "林林"
}
protocol CustomDataSeuqence: Sequence {}
extension CustomDataSeuqence {
func makeIterator() -> CustomIterator {
return CustomIterator(obj: self)
}
}
接下来比如我们要做一个社群功能,需要当前同一个社群当中的用户进行遍历,如果是VIP
,需要特殊显示一些效果。
那么这里的 Users
应该定义成什么类型呢?定义成 Any
类型,这个时候对于当前页面来说就需要讲 Any
的类型强转成 CustomDataSeuqence
,可以,但是有点牵强。这个时候我们就可以使用 AnySeuqence
let user = User(userId: 1001, name: "安安")
let vip = VIP()
let users: [AnySequence<String>] = [AnySequence(user), AnySequence(vip)]
for user in users{
// print(user)
for item in user {
print(item)
}
}
打印结果:
userId is 1001
name is 安安
vipdate is 20221226
viplevel is 18
vipName is 林林
这个时候 AnySequence
就将具体的 Sequence
类型隐藏了,调用者只知道数组中的元素是一个可以迭代输出字符串类型的序列。
三、泛型的底层原理
func testGenric<T>(_ value: T) -> T {
let tmp = value
return value
}
testGenric(10)
对于编译器来说,它不知道T
到底是什么具体的类型(可能是值类型,也可能是引用类型),那编译器就不知道 tmp / value
到底分配多少内存空间、步长是多少、对齐的字段是多少...只有当真正传递参数才知道真实类型对吧。
那tmp
在方法栈占据内存空间编译器是怎么决断的呢?
来看看上面代码编译成IR
代码进行分析:
可以看到在调用testGenric
时候可以看到一个值见证表valueWitnesses
,并且可以看出到initializeWithCopy、destroy
等函数都是从值见证表来的,所以泛型是通过ValueWitnessTable
去管理内存的。
截图是ValueWitnessTable
的数据结构,前面几个i8*其实就是void *代表内存管理函数,剩下的就是size、stride、flags、extraInhabitantCount,把数据结构还原一下
ValueWitnessTable
的数据结构:
// 弱引用表
struct ValueWitnessTable {
var unknow1: UnsafeRawPointer
var unknow2: UnsafeRawPointer
var unknow3: UnsafeRawPointer
var unknow4: UnsafeRawPointer
var unknow5: UnsafeRawPointer
var unknow6: UnsafeRawPointer
var unknow7: UnsafeRawPointer
var unknow8: UnsafeRawPointer
var size: Int
var stride: Int
var flags: UInt32
var extraInhabitantCount: UInt32
}
不管testGenric
函数的参数传递的是值/引用类型,不管什么类型,总之它的metadata
一定有ValueWitnessTable
!并且保存着这个类型的size、stride、flags、extraInhabitantCount
还有一些内存管理函数
等信息。
尝试还原一下上面分析的结构体对不对,举一个案例呗:
struct Teacher {
var age = 10
}
// 所有类型的最终基类 Metadata
struct TargetMetadata {
var kind: Int // kind用于区分类型的
}
ps: 不懂为啥是TargetMetadata可以看我之前f的分享的文章
let ptr = unsafeBitCast(Teacher.self as Any.Type, to: UnsafeMutablePointer<TargetMetadata>.self)
let valueWitnessTable = UnsafeRawPointer(ptr).advanced(by: -MemoryLayout<UnsafeRawPointer>.size).assumingMemoryBound(to: UnsafeMutablePointer<ValueWitnessTable>.self).pointee
print(valueWitnessTable.pointee.size) // 8
// 如果给 Teacher 添加一个属性 var age1 = 20,则打印的size就是 16
print(valueWitnessTable.pointee.unknow1) // 0x00007ff81b935400
print(valueWitnessTable.pointee.unknow2) // 0x00007ff81b935410
print(valueWitnessTable.pointee.unknow3) // 0x00007ff81b935420
print(valueWitnessTable.pointee.unknow4)
print(valueWitnessTable.pointee.unknow5)
print(valueWitnessTable.pointee.unknow6)
print(valueWitnessTable.pointee.unknow7)
print(valueWitnessTable.pointee.unknow8)
print("end")
ps: 为什么ValueWitnessTable
是在TargetMetadata
向前移动?
在IR代码中就能分析出,ValueWitnessTable
的位置是在TargetMetadata
的向前移动-1的位置。
来看看unknow1打印的是什么?
image.png得到函数名,可以自行打印最后恢复ValueWitnessTable
的数据结构:
// 弱引用表
struct ValueWitnessTable {
var initializeBufferWithCopyOfBuffer: UnsafeRawPointer
var destroy: UnsafeRawPointer
var initializeWithCopy: UnsafeRawPointer
var assignWithCopy: UnsafeRawPointer
var initializeWithTake: UnsafeRawPointer
var assignWithTake: UnsafeRawPointer
var getEnumTagSinglePayload: UnsafeRawPointer
var storeEnumTagSinglePayload: UnsafeRawPointer
var size: Int
var stride: Int
var flags: UInt32
var extraInhabitantCount: UInt32
}
在来对比刚才testGenric 编译后的IR代码里的,如果我们上面的结论:不管什么类型它的metadata
一定有ValueWitnessTable
是对的,那一定可以在IR上找到上面结构体里还原出来的函数。
恰好能对应上面还原的方法名:
总结:
- 泛型是通过
ValueWitnessTable
(简称VWT
是由编译器产生的) 来进行内存管理,VWT
存储了size、stride、alignment和一些内存管理函数等; - 对于值类型来说,实际上是通过内存的拷贝;copy/move等操作;
- 对于引用类型来说,就是堆区内存块的引用,通过引用计数的方式。
如果传递的参数是泛型呢?会发生什么变化?
来看看下面这个案例
func makeIncrementer() -> (Int) -> Int {
var runningTotal = 10
func incrementer(mount: Int) -> Int {
runningTotal += mount
return runningTotal
}
return incrementer
}
func genric<T>(t: T) {
}
let increment = makeIncrementer()
genric(t: increment)
编译成IR代码,找到mian函数:
define i32 @main(i32 %0, i8** %1) #0 {
entry:
%2 = alloca %swift.function, align 8
%3 = bitcast i8** %1 to i8*
// 调用makeIncrementer,生成闭包
%4 = call swiftcc { i8*, %swift.refcounted* } @"$s4main15makeIncrementerS2icyF"()
%5 = extractvalue { i8*, %swift.refcounted* } %4, 0
%6 = extractvalue { i8*, %swift.refcounted* } %4, 1
// 将指针存储到increment变量里
store i8* %5, i8** getelementptr inbounds (%swift.function, %swift.function* @"$s4main9incrementyS2icvp", i32 0, i32 0), align 8
// 将捕获的变量存储到increment变量里
store %swift.refcounted* %6, %swift.refcounted** getelementptr inbounds (%swift.function, %swift.function* @"$s4main9incrementyS2icvp", i32 0, i32 1), align 8
// 把function类型转换成void *
%7 = bitcast %swift.function* %2 to i8*
call void @llvm.lifetime.start.p0i8(i64 16, i8* %7)
// 从increment变量取出指针和捕获的变量
%8 = load i8*, i8** getelementptr inbounds (%swift.function, %swift.function* @"$s4main9incrementyS2icvp", i32 0, i32 0), align 8
%9 = load %swift.refcounted*, %swift.refcounted** getelementptr inbounds (%swift.function, %swift.function* @"$s4main9incrementyS2icvp", i32 0, i32 1), align 8
// 一些内存管理的函数
%10 = call %swift.refcounted* @swift_retain(%swift.refcounted* returned %9) #3
// swift_allocObject创建了堆区的内存空间
%11 = call noalias %swift.refcounted* @swift_allocObject(%swift.type* getelementptr inbounds (%swift.full_boxmetadata, %swift.full_boxmetadata* @metadata, i32 0, i32 2), i64 32, i64 7) #3
%12 = bitcast %swift.refcounted* %11 to <{ %swift.refcounted, %swift.function }>*
// 取出第一个成员是function这个结构体
%13 = getelementptr inbounds <{ %swift.refcounted, %swift.function }>, <{ %swift.refcounted, %swift.function }>* %12, i32 0, i32 1
// 从function这个结构体取出函数地址
%.fn = getelementptr inbounds %swift.function, %swift.function* %13, i32 0, i32 0
store i8* %8, i8** %.fn, align 8
%.data = getelementptr inbounds %swift.function, %swift.function* %13, i32 0, i32 1
store %swift.refcounted* %9, %swift.refcounted** %.data, align 8
%.fn1 = getelementptr inbounds %swift.function, %swift.function* %2, i32 0, i32 0
store i8* bitcast (void (%TSi*, %TSi*, %swift.refcounted*)* @"$sS2iIegyd_S2iIegnr_TRTA" to i8*), i8** %.fn1, align 8
%.data2 = getelementptr inbounds %swift.function, %swift.function* %2, i32 0, i32 1
store %swift.refcounted* %11, %swift.refcounted** %.data2, align 8
%14 = bitcast %swift.function* %2 to %swift.opaque*
// 后面又对这个function又做了一层重新的抽象
%15 = call %swift.type* @__swift_instantiateConcreteTypeFromMangledName({ i32, i32 }* @"$sS2icMD") #10
call swiftcc void @"$s4main6genric1tyx_tlF"(%swift.opaque* noalias nocapture %14, %swift.type* %15)
%.data3 = getelementptr inbounds %swift.function, %swift.function* %2, i32 0, i32 1
%16 = load %swift.refcounted*, %swift.refcounted** %.data3, align 8
call void @swift_release(%swift.refcounted* %16) #3
%17 = bitcast %swift.function* %2 to i8*
call void @llvm.lifetime.end.p0i8(i64 16, i8* %17)
ret i32 0
}
可以看到func genric<T>(t: T)
当泛型参数接收一个闭包/函数时,会对这个闭包/函数原有数据结构上又抽象封装了一层。因为我们传入可能是是函数,也可能传入的是闭包表达式,编译器不知道呀,它就和协议设计一样不知道谁遵循了协议,所以它就抽象了一层中间层为了泛型管理统一区分类型。
当去调用的时候,是通过这个抽象出来的中间层来取调用,如果不调用而用来传值的话,依旧是通过上面的ValueWitnessTable
进行内存管理的。(注意:函数/闭包也是引用类型)。
func genric<T>(t: T) {
// VWT管理内存
let tmp = t
// 函数调用,抽象中间层统一管理函数/闭包
var t1 = t as! (Int) -> Int
t1()
}
原本对于闭包的数据结构是这样的:(想了解可点击 函数/闭包数据结构)
// 闭包的实质
struct ClosureData<T> {
var ptr: UnsafeRawPointer // 闭包地址
var box: UnsafePointer<T> // Box
}
// 捕获单个外部变量的时候Box
struct Box<T> {
var heapObject: HeapObject // 实例对象的内存地址
var value: T
}
// 捕获多个外部变量的时候Box
//struct Box<T1, T2> {
// var object: HeapObject // 把value1、value2统一当成object的属性
// var value1: UnsafePointer<T1>
// var value2: T2
// var value3.......
//}
struct HeapObject {
var metadata: UnsafeRawPointer
var refcount1: UInt32
var refcount2: UInt32
}
泛型参数接收闭包/函数后对其进行调用,又抽象一层封装的数据结构:
// 闭包作为泛型参数,又包装了一层
struct ReabstractionThunkContext<Context> {
var heapObject: HeapObject
var function: ClosureData<Context>
}
验证数据结构:
func makeIncrementer() -> (Int) -> Int {
var runningTotal = 10
func incrementer(mount: Int) -> Int {
runningTotal += mount
return runningTotal
}
return incrementer
}
func genric<T>(t: T) {
let ptr = UnsafeMutablePointer<T>.allocate(capacity: 1)
ptr.initialize(to: t)
defer {
ptr.deinitialize(count: 1)
ptr.deallocate()
}
let closure_ptr = ptr.withMemoryRebound(to: ClosureData<ReabstractionThunkContext<Box<Int>>>.self, capacity: 1) { $0 }
let ctx = closure_ptr.pointee.box.pointee.function.box
print(ctx.pointee.value) // 10 得到捕获的外部变量的值
}
let increment = makeIncrementer()
genric(t: increment)