英文:generic
之前看别人代码,有大量使用泛型,感觉自己有些模糊,就仔细的研究下。
总的来说,在swift 中能极大的减少代码量,和简化代码。
-
泛型介绍
他的作用有下面案例来引入:
func compareMax(a: Int, b: Int) -> Int {
a > b ? a : b
}
func compareMax(a: Double, b: Double) -> Double {
a > b ? a : b
}
func compareMax(a: Float, b: Float) -> Float {
a > b ? a : b
}
那这种比较方法就太多了 ,那swift就引入了泛型的概念 泛型函数
func compareMax<T: Comparable>(a: T, b: T) -> T {
a > b ? a : b
}
- <>就是固定的语法糖,与oc 基本一致,表示<>里面的类型是泛型
- 在该方法中 T(Type parameter)是泛型类型的代称,(可以看成是某种类型,现在还没确定下来),自己也可以定义成其他有意义的名字,或者简称。
- :Comparable 是对泛型的限制,也就传来的类型T 必须遵循 Comparable 协议
- 方法入参参数 a与 b 是同一种类型
- 方法出参入参是同一种类型
在使用的时候
compareMax(a: 1, b: 2)
泛型确认成Int类型
compareMax(a: 1.1, b: 2.2)
泛型确认成Double类型
== 和 != 是否相等需要遵循 Equatable
< 比较需要遵循 Comparable
// 泛型 - 泛指某种类型
在声明的时候类型并没有定下来,不确定是什么类型,在使用的时候才能确认是什么类型
方法可传不同的类型的参数,那泛型也可以同时定义多个泛型,注意:多个泛型需要使用“,”隔开 如下:
func demo<T:Comparable, U:Equatable >(a: T, b: U) {... }
2. ### 泛型类型
Swift中使用的类型都是封装好的类型
比如Int类型表面是 是Int 类型,实际上是 struct 类型,那有泛型的类型又叫泛型类型。
就比如说Array 就是泛型类型
上面的例子是方法带泛型交泛型方法。
有泛型的类型叫泛型类型
注意:结构体和枚举类定义的类型参数,需要在创建实例的时候,确认泛型类型。
-
在swift 标准库中泛型的使用
1.数组 Array
Array的声明
其中Element 就是一个泛型,而且该类型没有任何的约束,可以是枚举,结构体,对象等,
那这样定义的好处就是,不需要在底层定义各种类型的Array ,抽象成一种带泛型的 Array
使用了有意义的Element 来定义泛型的名称
Array的实例化
// 定义数组
let listTmp: Array<Int> = Array<Int>()
let listTmp: Array<Int> = [1,2,3]
let list: [Int] = [1,2,3]
2.字典
Dictionary的声明
其中<> 里面的 key、value 就是泛型 - 一种类型,具体干什么还不清楚。
其中Key有限制-需要遵循 Hashable 协议
where 后面会将
咱们知道字典的存储的数据是Key: value 的类型,那swift 把存储的数据类型,抽象成2个泛型。其中key 是遵循了Hashable协议
Hashable 是 Swift 标准库中的一个协议,它扩展了 Equatable 协议。遵循 Hashable 协议的类型必须提供一种方法来计算其哈希值,确保值的唯一性。
字段的实例化
let dic: Dictionary<String,Any> = Dictionary<String,Any>()
let dicTmp: [String: Any] = [:]
@frozen 是一个与结构体(struct)和枚举(enum)相关的属性。它主要用于优化编译器生成的代码,并且对 ABI(应用程序二进制接口)稳定性有影响。
@frozen 属性告诉编译器,在编译时确保该类型的所有字段和布局是固定的,不允许在未来的版本中添加或移除存储属性,也不允许改变现有属性的顺序或类型。类型的属性可以直接通过偏移量进行,而不需要额外的间接层,减少运行时开销,特别是在频繁访问结构体或枚举属性的情况下.
public 是访问控制修饰符
Swift 中的访问控制
-
private:仅在同一源文件内的同一作用域内可用。
- 并尽可能地利用
private来保护实现细 - 建议在swift 中多使用该字符,有助于提升代码运行速度
- 使用
private(set)提供受控访问- 提供只读属性
- 并尽可能地利用
-
fileprivate:在同一源文件中的任何地方都可用,但不在文件外。
-
internal(默认):在同一模块中的任何源文件中都可用。
-
public:不仅在同一模块中的任何源文件中可用,而且在导入该模块的其他模块中也可见。
-
open:与
public类似,但它允许类被继承,以及类成员被重写,在其他模块中也可被重写其成员 -
泛型类型的约束
struct Stack<Element: Equatable> { var list:[Element] = [Element]() mutating func addEle(newEle: Element) { list.append(newEle) } } 1.这段代码中 Element 是泛型(一般是指类型,建议都要大驼峰写法) 2. Equatable就是对泛型 Element 的约束,(约束可以是协议,也可是某种类型),若是某种类型的话,可以传其子类(多态用法)
泛型后面使用: 直接协议或者具体类型,是比较简单的泛型约束,还有其他的约束: where
where 字句来对泛型进行约束
简单的用法:
看这个代码:
Dictionary 有2个泛型 Key和Value 其中附带了where 字句来进一步对泛型Key作出约束,必须遵循Hashable协议。
一般写在方法、类、枚举、结构体等具体实现前也就是在“{” 前面。
复杂的用法
1.遵循多个协议
struct ModelX<M: Equatable> where M: Comparable {
}
定义
var tmp = ModelX<String>()
- 针对不同类型添加不同方法
struct ModelX<M: Equatable> where M: Comparable {
var model: M
}
extension ModelX where M == Int {
func toString() -> String {
return String(model)
}
}
这里面的== 是指类型要一致
这里的: 是指遵循协议或是摸个类型
基本上常用的就这2用
3. 针对协议的关联属性再进行约束
struct ModelX<M: Sequence> where M.Element: Comparable {
var model: M?
}
定义
var model = ModelX<[Int]>()
关于泛型约束 总体分3种
-
要求类型实现某个协议
where Element: Equatable -
要求类型是某个类的子类
where Element: UIView -
要求类型是某个具体的类型值
where Element == String -
同时遵循多个协议用法
使用&
5.1 泛型函数遵循多个协议
func test<T: Comparable & Codable>(_ a: T, _ b: T) { 」
5.2泛型类型遵循多个协议
struct Container<T: Equatable & Codable> {
var items: [T]
}
使用 where 子句
func test<T>(items: [T]) where T: Comparable & Codable {}
6. ### 协议中的泛型 - associatedtype 关联类型
protocol ModelProtocol {
associatedtype Item: Comparable
func add(item: Item)
}
protocol ModelProtocol {
associatedtype Item: Sequence where Item.Element: Comparable
func add(item: Item)
}
与函数中的泛型定义与使用都不太一样
使用如下
struct ModelStruct: ModelProtocol {
typealias Item = Int
func add(item: Item){}
func dele(item: Item){}
}
或者
struct ModelStruct: ModelProtocol {
// 协议的泛型 - 实现协议同名的方法, 编译器即可以推算出 协议中的Item 就是Int
func add(item: Int) {
}
}
使用 typealias 来确认协议的关联属性 ,在struct 中Item 的类型就是 Int 类型,
比较泛型与协议的实例的差异
// 类或结构体 泛型
var tmp = ModelX<String>()
struct ModelStruct: ModelProtocol {
// 协议的泛型
typealias Item = Int
或者
func add(item: Int) {
}
}
7. ### 了解下系统怎么处理Generics
Swift 对 Generics 的处理分 2 种情况:
-
编译时,对 Generics 做特化处理 (Specialization)
-
运行时,对 Generics 做装箱处理 (Boxing)
**编译时-**特化处理
是在编译的时候知道泛型的类型,生成泛型的特定版本,将泛型转换为非泛型,
简单说就是生成不同类型的方法如下:使用Int 类型调用就生成入参是Int 类型的方法。
@inline(never)
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}
var a = 1
var b = 2
swapTwoValues(&a, &b)
swiftc -emit-sil -O test.swift -o output.sil
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
//alloc_global 分配一个全局变量
alloc_global @$s6output1aSivp // id: %2
// 获取全局变量的地址,并将这个地址存储到 %3
//%3 代表的是值的标识符。这些标识符是编译器内部用于跟踪计算结果或变量位置的临时名称
%3 = global_addr @$s6output1aSivp : $*Int // users: %6, %12
//创建一个整数字面量 1,类型为 Builtin.Int64
%4 = integer_literal $Builtin.Int64, 1 // user: %5
//将 Builtin.Int64 包装成 Int 类型,结果存储在 %5
%5 = struct $Int (%4 : $Builtin.Int64) // user: %6
//将 %5 中的值存储到全局变量 a 的地址 %3。
store %5 to %3 : $*Int // id: %6
alloc_global @$s6output1bSivp // id: %7
%8 = global_addr @$s6output1bSivp : $*Int // users: %11, %13
%9 = integer_literal $Builtin.Int64, 2 // user: %10
%10 = struct $Int (%9 : $Builtin.Int64) // user: %11
store %10 to %8 : $*Int // id: %11
//begin_access:开始访问全局变量 a 和 b,并获得它们的可修改地址。
[modify] 表示允许修改,
[dynamic] 表示动态作用域,
[no_nested_conflict] 表示没有嵌套冲突
%12 = begin_access [modify] [dynamic] [no_nested_conflict] %3 : $*Int // users: %15, %17
%13 = begin_access [modify] [dynamic] [no_nested_conflict] %8 : $*Int // users: %15, %16
//获取特化版本的 swapTwoValues 函数引用
// function_ref specialized swapTwoValues<A>(_:_:)
%14 = function_ref @$s6output13swapTwoValuesyyxz_xztlFSi_Tg5 : $@convention(thin) (@inout Int, @inout Int) -> () // user: %15
//apply:调用 swapTwoValues 函数,并传递两个全局变量 a 和 b 的地址作为参数
%15 = apply %14(%12, %13) : $@convention(thin) (@inout Int, @inout Int) -> ()
@inline(never) 是 Swift 中的一个函数属性,用于指示编译器不要内联指定的函数。
内联是一种编译优化,它将函数调用替换为函数体本身,从而减少函数调用的开销,但在某些情况下可能会增加代码体积。通过使用 @inline(never),你可以明确告诉编译器避免这种优化,这在调试或性能分析时特别有用。
另外,在编译时若开启了 Whole-Module Optimization,同一模块内部的泛型调用也可以被特化。现在xcode 该模式是默认打开的。
运行时 -装箱处理
简单来说,就是要对 Generics 做一次封装转换,Generics 在使用时真实类型可能千差万别,但 Generics 定义是需要有「固定的对象模型」。
所谓对象模型 (Object Model),主要有几个职责:
-
规范对象实例化时属性如何存储
-
规范对象如何执行 allocate、copy、destroy 等基础内存操作以及获取 size、alignment 等内存信息
-
规范如何查找实例方法的入口地址
查看 IR
//计算了一个相对于基地址 %T 的偏移量 -1,以获取值见证表(value witness table)的指针。
%2 = getelementptr inbounds ptr, ptr %T, i64 -1
//load:从内存中加载值见证表的指针。
%T.valueWitnesses = load ptr, ptr %2, align 8, !invariant.load !18, !dereferenceable !19
(value witness table) 值见证表通常包含以下几种关键的操作: initializeBuffer(to:):初始化一块内存区域,并将给定的值复制到其中。 destroy:销毁一个值类型实例,释放其占用的资源。 copy:复制一个值类型实例到新的内存位置。 assign:将一个值类型实例赋值给另一个实例,可能涉及深层复制。 getSize:获取值类型的大小。 getAlignment:获取值类型的对齐要求。 takeUnretainedValue 和 storeUnretainedValue:用于处理引用计数,特别是在与 Objective-C 桥接时。
根据泛型约束 可以分为三种情况:
- 没有约束类型
这类泛型能做的事非常少,Boxing 只需关心 allocate、copy、destroy 等基本操作如何执行即可
-
有协议来约束
-
除了 allocate、copy、destroy 以外,还需要通过 PWT (Protocol Witness Table) 存储协议中指定的方法,以便通过 generic-types 可以调用它们
-
-
有具体类型约束
有基础类作为约束,除了 allocate、copy、destroy 以外,还需要通过 VWT (Value Witness Table) 存储约束类中定义的方法,以便通过 generic-types 可以调用到它们
总之,Generics 对性能有影响,主要体现在 2 个方面:
- Boxing 处理
- 通过 Generics 调用的方法都是动态派发 (通过 VWT 或 PWT)
总结:
-
尽量使用具体类型:当你定义泛型函数或类型时,如果知道具体的使用场景,尽量使用具体类型代替泛型参数。这样可以让编译器更好地优化代码。
-
避免过度泛化:虽然泛型提供了灵活性,但过多的泛型参数可能会影响编译器的优化能力。
-
审查依赖关系:确保泛型代码中的依赖关系尽可能简单明了,以便编译器能够有效地推断和特化类型。
项目中使用
@propertyWrapper
public struct UserDefaultWrapper<T> {
public var wrappedValue: T? {
get {
UserDefaults.standard.value(forKey: key) as? T ?? defValue
}
set {
UserDefaults.standard.setValue(newValue, forKey: key)
UserDefaults.standard.synchronize()
}
}
private var key: String
private var defValue: T?
public init(key: String, defValue: T?) {
self.key = key
self.defValue = defValue
}
}
使用
@UserDefaultWrapper(key: "kQCTopicClearLeftGuideKey_(USERINFO.accountId ?? "")",
defValue: false)
private var showLeftSlideGuide: Bool?
showLeftSlideGuide == false
showLeftSlideGuide = true
8. ### 泛型的推断
extension Bundle {
func decode<T: Codable>(file:String) -> [T] {
guard let url = Bundle.main.url(forResource: file, withExtension: "json") else { fatalError("meizhaod") }
guard let data = try? Data(contentsOf: url) else { fatalError("meizhaod") }
guard let decoder = try? JSONDecoder().decode([T].self, from: data) else { fatalError("meizhaod") }
return decoder
}
}
在使用的是
let xx=Bundle.main.decode("plans")
这样是错误的需要指定 xx 的类型, 才能推断出T的类型
因为T需要传 Codable 协议的类型,需要接的时候使用具体类型,才能推算出来具体类型。