swift 泛型

0 阅读11分钟

英文:generic

之前看别人代码,有大量使用泛型,感觉自己有些模糊,就仔细的研究下。

总的来说,在swift 中能极大的减少代码量,和简化代码。

  1. 泛型介绍

他的作用有下面案例来引入:

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
    }
   
  1. <>就是固定的语法糖,与oc 基本一致,表示<>里面的类型是泛型
  2. 在该方法中 T(Type parameter)是泛型类型的代称,(可以看成是某种类型,现在还没确定下来),自己也可以定义成其他有意义的名字,或者简称。
  3. :Comparable 是对泛型的限制,也就传来的类型T 必须遵循 Comparable 协议
  4. 方法入参参数 a与 b 是同一种类型
  5. 方法出参入参是同一种类型

在使用的时候

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 就是泛型类型

上面的例子是方法带泛型交泛型方法。

有泛型的类型叫泛型类型

注意:结构体和枚举类定义的类型参数,需要在创建实例的时候,确认泛型类型。

  1. 在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 中的访问控制

  1. private:仅在同一源文件内的同一作用域内可用。

    1. 并尽可能地利用 private 来保护实现细
    2. 建议在swift 中多使用该字符,有助于提升代码运行速度
    3. 使用 private(set) 提供受控访问- 提供只读属性
  2. fileprivate:在同一源文件中的任何地方都可用,但不在文件外。

  3. internal(默认):在同一模块中的任何源文件中都可用。

  4. public:不仅在同一模块中的任何源文件中可用,而且在导入该模块的其他模块中也可见。

  5. open:与 public 类似,但它允许类被继承,以及类成员被重写,在其他模块中也可被重写其成员

  6. 泛型类型的约束

     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>()
    
  1. 针对不同类型添加不同方法
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种

  1. 要求类型实现某个协议where Element: Equatable

  2. 要求类型是某个类的子类 where Element: UIView

  3. 要求类型是某个具体的类型值 where Element == String

  4. 同时遵循多个协议用法

使用&

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 桥接时。

根据泛型约束 可以分为三种情况:

  1. 没有约束类型

这类泛型能做的事非常少,Boxing 只需关心 allocate、copy、destroy 等基本操作如何执行即可

  1. 有协议来约束

    1. 除了 allocate、copy、destroy 以外,还需要通过 PWT (Protocol Witness Table) 存储协议中指定的方法,以便通过 generic-types 可以调用它们

  2. 有具体类型约束

有基础类作为约束,除了 allocate、copy、destroy 以外,还需要通过 VWT (Value Witness Table) 存储约束类中定义的方法,以便通过 generic-types 可以调用到它们

总之,Generics 对性能有影响,主要体现在 2 个方面:

  • Boxing 处理
  • 通过 Generics 调用的方法都是动态派发 (通过 VWT 或 PWT)

总结:

  1. 尽量使用具体类型:当你定义泛型函数或类型时,如果知道具体的使用场景,尽量使用具体类型代替泛型参数。这样可以让编译器更好地优化代码。

  2. 避免过度泛化:虽然泛型提供了灵活性,但过多的泛型参数可能会影响编译器的优化能力。

  3. 审查依赖关系:确保泛型代码中的依赖关系尽可能简单明了,以便编译器能够有效地推断和特化类型。

项目中使用

 @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 协议的类型,需要接的时候使用具体类型,才能推算出来具体类型