[Swift 初学笔记] 三: 类型擦除的理解

1,253 阅读5分钟

类型擦除

通过泛型协议,我们了解到编译器无法处理编译期不确定类型的泛型协议作为类型使用, 为了能绕过强类型检查, 可以通过类型擦除实现 类型擦除就是在代码中 让抽象类型具体化 的一个过程 我对类型擦除的理解: 由于类型泛型编译器会针对每一种具体类型生成对应的类型定义, 所以新增一个中间类型实现泛型协议, 针对每一个具体的类型, 最终都只会存在一种类型遵循了泛型协议, 即泛型协议中关联类型不会再有二义性, 然后中间类通过消息转发,将实际调用关系转发给被包装的类 整个就是类型擦除的实现思路 参考Swift系统库中的定义, 一般类型擦除都是以Anyxxxx定义,代码示例如下:

隐式引用base

在新定义的Anyxxx类中, 无法直接显示的引用传入的base(无论base 定义为T类型,还是Printer) 实例代码:

Protocol Printer
{
    associated T
    func print(var : T)
}

struct AnyPrinter<U> : Printer {
    typelias T = U

// 函数指针
    private let _print:(U) -> ();
    init<Base : Printer>(base:Base) where Base.T == U {
        _print = base.print
    }
    
    func print(val :T)
    {
        _print(val)
    }
}
  • AnyPrinter 遵循Printer的目的是能够内容实现print方法, 以实现转发
  • AnyPrinter 内部通过函数指针实现对base的隐式转发

这种方式的优点就是简单, 缺点也很明显, 如果协议中存在很多函数, 那么就需要定义大量的函数指针,实现消息的转发, 针对这一缺点, 优化思路就是采用实例的显式引用

显式引用base

参考文章类型擦除- 楚权中提到, base无论定义成Printer还是Printer都会编译报错, 那么有没有办法实现显式引用base呢, 是有的 参考swift系统库中Codable 一般以Box命名

// 泛型协议
protocol Printer {
    associatedtype T
    func myprint(val: T)
}

// 类型擦除,至少需要两个泛型类型, 一个T用于绑定泛型协议的关联类型, 一个Base用于约束需要显式引用的实例的类型
// 两个泛型分别是通过泛型类型和泛型函数实现的,
// 这里就造成了一个问题, swift是强类型语言, 成员变量要求声明时就指明类型, 但是绑定实例是通过泛型函数实现的,声明变量box时是没办法使用泛型函数的类型的, 解决办法就是通过子类对基类的约束, 实现变量类型约束的后置声明, 实现如下:
// 再次创建一个中间类_AnyPrinterBoxBase, 作用同AnyPrinter一样, 实现子类, 用于包装实例类型, 所以子类泛型要遵循协议的约束
struct AnyPrinter<T>: Printer {
    // 显式引用, 为了实现显式引用, 需要生成两个类_AnyPrinterBoxBase和_PrinterBox
    // 这里显式引用的难度在于无法定义box类型, 所以只能新增一个桥接层, 实现类型二次擦除
    // 二次擦除后, 通过_PrinterBox <==> _AnyPrinterBoxBase多态实现类型绑定
    var _box: _AnyPrinterBoxBase<T>

    // 泛型函数, 将子类包装成子类
    init<Base: Printer>(_ base: Base) where Base.T == T {
        _box = _PrinterBox(base)
    }

    // 消息转发
    func myprint(val: T) {
        _box.myprint(val: val)
    }
}
// 显式引用base, 定义的基类
// BoxBase的作用在于能在类型擦除函数中,提前声明base类型, 对该泛型类型中的泛型E是没有Printer协议约束的
class _AnyPrinterBoxBase<E>: Printer {
    typealias T = E

    func myprint(val: E) {
        fatalError()
    }
}

// 定义Box子类, 继承自基类_AnyPrinterBoxBase
// 子类的泛型类型是有协议约束的, 目的在于将泛型类型的约束绑定到 泛型协议上的T
class _PrinterBox<Base: Printer>: _AnyPrinterBoxBase<Base.T> {
    // Base 遵循了Printer协议约束, 用来包装遵循Printer协议的类型
    // _AnyPrinterBoxBase<Base.T> Base.T将真正的实例类型绑定到泛型协议的关联类型上
    var base: Base

    init(_ base: Base) {
        self.base = base
    }

    override func myprint(val: Base.T) {
        base.myprint(val: val)
    }
}

正如注释中解释的, 显式依赖遇到的问题就是类型约束的时机绑定问题(即将Base.T绑定到T上), 解决方案也是针对这一问题, 通过子类对基类的约束, 实现变量类型约束的后置

另一种显式引用base的方式

该方式算类型擦除的一种变体, 相比正式的类型擦除, 使用上会有区别

// 泛型协议
protocol Printer {
    associatedtype T
    func myprint(val: T)
}

// 类型擦除
struct AnyPrinter<Base: Printer>: Printer {
    
    typealias T = Base.T
    
    var _box: Base
    
    // 将子类包装成子类
    init(_ base: Base) {
        _box = base
    }
    
    // 消息转发
    func myprint(val: T) {
        _box.myprint(val: val)
    }
}

class MyClass : Printer
{
    typealias T = Int
    func myprint(val: Int) {
        print(val + val)
    }
}
// error: 无法直接使用Int作为泛型的具体类型
let mystruct : AnyPrinter<Int> = AnyPrinter(MyClass())
        mystruct.myprint(val: 20)
        
let mystruct : AnyPrinter<MyClass> = AnyPrinter(MyClass())
        mystruct.myprint(val: 20)

将Box和Any结合起来的思路, 类型擦除的目的, 无非就是通过类型绑定和泛型类型中间桥接来解决泛型协议无法直接作为类型使用的场景, 所以如果不需要严格意义的类型擦除, 只是为了使用泛型协议

总结

如果想理解类型擦除具体解决了什么问题, 可能要扩展了解swift的泛型类型和泛型协议的底层实现原理, 后续章节会继续学习相关模块, 欢迎一起交流学习

参考

类型擦除- 楚权