Swift泛型、类型擦除

879 阅读7分钟

1、泛型

  • 泛型可以将类型参数化,提高代码抽象能力和复用性

1.1、泛型的存在意义

  • 我们先来看一个非泛型的函数
    func multiNumInt(_ x: Int, _ y: Int) -> Intreturn x * y 
    }
    
    这个函数计算了2个Int型参数的乘积,但是仅限于Int型,如果此时我们有需求计算Double型的乘积,难道要再写一个只有形参类型改成Double的函数吗?显然不划算,此时我们就需要使用泛型

1.2、泛型语法书写

  • 在函数名后面的一对尖括号中写入自定义的类型 T ,类型参数后面放置协议或者是类表示 类型约束(当前我们这个 T 要遵循 FloatingPoint 协议,计算乘积所必须);然后我们就可以使用 T 来替换任意定义的函数形式参数
    //常规泛型
    func multiNum<T: FloatingPoint>(_ x: T, _ y: T) -> Treturn x * y 
    }
    
    //泛型栈结构
    struct Stack<T>{
            private var items = [T]()
            
            mutating func push(_ item: T){
                items.append(item)
            }
            mutating func pop() -> T?{
               if items.isEmpty {
                   return nil
               }
              return items.removeLast()
        }
    }
    

1.3、关联类型

  • 当我们遇到几个类、结构体等有相同的操作时,我们免不了抽取相同的行为定一个协议,但对于协议是不支持泛型参数的声明的 image.png 那么要使协议具有泛型的特性,需要使用 关联类型,并可给当前的关联类型添加约束,比如要求 Item 必须都要遵循 FixedWidthInteger;下边是将上边泛型栈结构进行提取形成的协议:
    protocol StackProtocol { 
        associatedtype T:FixedWidthInteger
        
        var itemCount: T{ get }
        mutating func push(_item : T)
        mutating func pop() -> T?
        func index(of index: Int) -> T 
    }
    
    关联类型 会使用associatedtype关键字给协议中用到的类型一个占位符名称(示例中为 T),协议定义中的相关类型我们都可以用这个占位符(T)替代,等到真正实现协议的时候在去确定当前占位符的类型
    struct LZStack:StackProtocol {
        typealias T = Int      //将协议中的关联类型占位符赋予实际的类型
    
        var items = [T]()      //使用被赋予实际类型的T创建数据类型
    
        var itemCount: Int {
            get {
                return items.count
            }
        }
    
        mutating func push(_ item: T) {
            items.append(item)
        }
    
        mutating func pop() -> T? {
            if items.isEmpty {
                return nil
            }
            return items.removeLast()
        }
    
        func index(of index: Int) -> T {
            return items[index]
        }
    }
    
    var s = LZStack()
    s.push(10)
    s.push(20)
    s.push(50)
    s.pop()
    print(s.items)
    
    //打印结果
    [10, 20]
    

1.4、where

  • where语句主要用于表明 泛型需要满足的条件,即限制形式参数的要求
    protocol EvenProtocol: StackProtocol{
        associatedtype Even: EvenProtocol where Even.T == T 
        func pushEven(_ item: Int) -> Even
    }
    

1.5、类型擦除

  • 为了了解什么是类型擦除,我们先准备一段根据不同业务类型,获取不同数据的代码

    //获取数据泛型协议
    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<dataType, Error>) -> Void)?) {
            let user = User(userId: 1001, name: "Normal")
            completion?(.success(user))
        }
    }
    
    //获取VIP用户数据
    struct VIPData: DataFetch {
        typealias dataType = User
        
        func fetch(completion: ((Result<dataType, Error>) -> Void)?) {
            let user = User(userId: 2001, name: "VIP")
            completion?(.success(user))
        }
    }
    
    • 如果这时候我们需要在 controller 中使用 DataFetch 协议获取我们当前的 User 数据,运行后你会发现会报错,也就意味着 DataFetch协议只能用作泛型约束,不能用作具体类型!因为编译器无法确定 dataType 的具体类型是什么! image.png

    • 这时可能有人会说将 DataFetch协议改成具体 UserData 不就行了 image.png 这样行吗?确实能过编译,但是这会在 ViewController 和 UserData 对象之间创建一个依赖关系,增加里其中的耦合度,因此最好的方式是在引入一个 中间层

  • 我们定义一个中间层结构体 AnyDataFetch , AnyDataFetch 实现 DataFetch 的所有方法。

    //泛型协议
    protocol DataFetch {
        associatedtype dataType
    
        func fetch(completion: ((Result<dataType, Error>) -> Void)?)
    }
    
    //用户数据
    struct User {
        let userId: Int
        let name: String
    }
    
    //中间层结构体
    struct AnyDataFetch<T>:DataFetch {
        typealias dataType = T
    
        private let _fetch:(((Result<dataType,Error>) -> Void)?) -> Void
        
        init<U: DataFetch>(_ fetchable: U) where U.dataType == T {
            //需要实现的类型在此处记录(依赖注入)
            _fetch = fetchable.fetch(completion:)
        }
    
        func fetch(completion: ((Result<T, Error>) -> Void)?) {
            //转发协议的抽象类型
            _fetch(completion)
        }
    }
    
    struct UserData: DataFetch {
        typealias dataType = User
    
        func fetch(completion: ((Result<dataType, Error>) -> Void)?) {
            let user = User(userId: 1001, name: "LZ")
            completion?(.success(user))
        }
    }
    
    struct VIPData: DataFetch {
        typealias dataType = User
    
        func fetch(completion: ((Result<dataType, Error>) -> Void)?) {
            let user = User(userId: 2001, name: "VIP")
            completion?(.success(user))
        }
    }
    
    class someViewController {
        let userData : AnyDataFetch<User>
        init(_ data : AnyDataFetch<User>) {
            self.userData = data
        }
    }
    
    let userVip = VIPData()
    let anyFetchUser = AnyDataFetch<User>(userVip)
    let vc = someViewController.init(anyFetchUser)
    
    vc.userData.fetch { (result) in
        switch result {
        case .success(**let** user):
            print(user.name)
        case .failure(**let** error):
            print(error)
        }
    }
    
    • 在 AnyDataFetch 的初始化过程中,实现协议的类型会被当做参数传入依赖注入
    • 在 AnyDataFetch 实现的具体协议方法 fetch 中,再转发实现协议的抽象类型
  • 对于 someViewController , 我接收的其实还是 AnyFetchable<User> 类型,这其实就是所谓的 类型擦除;系统中的 AnySequence , AnyCollection 都是这样的原理

2、泛型原理

  • 分析泛型原理需要查看底层 IR 代码,内容不过多赘述,关键的内容是下边部分:

    // 这个 valueWitnesses 是一个值见证表 
    // valueWitnesses 记录了 size、stride、aligment 以及内存管理的一些函数。 
    %T.valueWitnesses = load i8**, i8*** %4, align 8, !invariant.load !15, !dereferenceable !16
    

    泛型函数是通过一个叫 ValueWitnessTable 的东西来管理泛型函数的内存的;它的内存结构为:

    %swift.vwtable = type { i8*, i8*, i8*, i8*, i8*, i8*, i8*, i8*, i64, i64, i32, i32 }
    

    这种 i8* 我们都可以把它当作 void*,也就是这些 i8* 其实就是当前所谓的函数

  • 直接给出结论,ValueWitnessTable 的结构是:

    struct ValueWitnessTable {
        var initializeBufferWithCopy: 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
    }
    
  • 当我们把闭包或者函数当作泛型参数进行传值的时候,它为了使泛型的管理统一,也是重新抽象了一层中间层来捕获当前传进来的函数;分辨当前传入的是闭包还是函数是通过 metadata 来进行判断的

3、AnySequence

  • 假设你有这样一个需求,你需要迭代你的自定义属性 User ,其中 User 属性如下:
    struct Uservar 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 类型隐藏了,调用者只知道数组中的元素是一个可以迭代输出字符串类型的序列