Swift 类型擦除

2,233 阅读5分钟

原文链接

《Swift 泛型协议》 中,我们探讨了如何基于类型擦除技术解决 Swift 泛型协议的存储问题,通过定义一个类型擦除包装器 AnyPrinter 解决了泛型协议 Printer 的存储问题。但是,AnyPrinter 并没有显式地引用 base 实例,因为当我们定义一个泛型类型的属性时,编译器会报错。

如果我们在 AnyPrinter 中定义一个 base 属性用于显式引用实例。当我们将 base 声明为 Printer,编译器会报错:Cannot specialize non-generic type 'Printer';当我们将 base 声明为 Printer<T>,编译器会报错:Protocol 'Printer' can only be used as a generic constraint because it has Self or associated type requirements。如下所示。

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

    var base: Printer<T>
    // Error: Protocol 'Printer' can only be used as a generic constraint because it has Self or associated type requirements
    
    var base: Printer
    // Error: Cannot specialize non-generic type 'Printer'

    init<Base: Printer>(base : Base) where Base.T == U {
        self.base = base
    }

    func print(val: T) {
        base.print(val)
    }
}

最终我们基于方法指针隐式地引用了 base 实例。如下所示。

struct AnyPrinter<U>: Printer {
    typealias T = U
    private let _print: (U) -> ()

    init<Base: Printer>(base : Base) where Base.T == U {
        _print = base.print
    }

    func print(val: T) {
        _print(val)
    }
}

本文,我们就来探讨一下,在泛型协议中,如何显式地引用 base 实例。

protocol Printer {
    associatedtype T
    func print(val: T)
}

中间类型

上述实现中,在 AnyPrinter 中定义了一个 base 属性,在声明其类型时,无论是声明为 Printer<T> 还是 Printer,编译器都会报错。为了解决这个问题,我们还是以那句经典名言为指导思想,实现一个包装类型作为 base 属性的类型

这里我们需要另外定义两个类型,两者是基类和子类的关系,并且都遵循泛型协议 Printer。至于为什么定义两个类型,我们后面再解释。在 Swift 标准库实现中,经常使用 box 命名中间类型,或者说是盒子类型、包装类型,这里我们同样以 box 进行命名。

Box 基类

如下所示为 box 基类的实现,由于泛型类型 _AnyPrinterBoxBase 遵循了 Printer 泛型协议,类型参数会自动绑定至关联类型。在真正使用时,_AnyPrinterBoxBase 并不会保持抽象,它最终会被绑定到某个特定类型。

class _AnyPrinterBoxBase<E>: Printer {
    typealias T = E

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

Box 子类

如下所示为 box 子类的实现,其内部封装了一个实例 var base: Base,并且将方法传递给了实例。这个 base 实例才是 Printer 协议真正的实现者。在 _PrinterBox 类型声明的第一行中,其自动将 Base.TPrinter 协议的关联类型)绑定为 _AnyPrinterBoxBase.T_AnyPrinterBoxBase 的类型参数) 。此时,我们也无需再在 _PrinterBox 内部通过 typealias T == xxx 的方式手动进行类型绑定。

class _PrinterBox<Base: Printer>: _AnyPrinterBoxBase<Base.T> {
    var base: Base
    
    init(_ base: Base) {
        self.base = base
    }

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

类型擦除

在实现了中间类型后,我们再来修改类型擦除包装器 AnyPrinter 的内部实现。具体如下所示,由于我们使用中间类型 box 对 base 进行了封装,所以这里我们需要将 AnyPrinter 中的 base 的命名修改为 _box。当我们调用 print 方法时,其内部会将 print 方法转发至 _box,而 _box 内部又会将 print 转发至 base 这个真正的实现者。

struct AnyPrinter<T>: Printer {
    var _box: _AnyPrinterBoxBase<T>

    init<Base: Printer>(_ base: Base) where Base.T == T {
        _box = _PrinterBox(base)
    }

    func print(val: T) {
        _box.print(val: val)
    }
}

现在,我们再来看前文留下的问题:为什么中间层需要定义基类和子类两个类型。事实上一开始,我尝试只定义一个 box 类型 _PrinterBox,如下所示,但是编译器会报错:

class _PrinterBox<Base>: Printer {
    typalias T = Base
    var base: Base

    init<Base: Printer>(_ base: Base) where Base.T == T {
        self.base = base
        // Error: Cannot assign value of type 'Base' to type 'Base'
    }

    func print(val: Base) {

    }
}

这个报错看上去有点奇怪,我猜测其原因:虽然构造器通过 where Base.T == TBase 类型进行了约束,但是却并没有将 Printer.T 绑定至 Base 类型。不过奇怪的是,我加了 typealias T = Base 也不管用。如果有人知道原因,可以留言告诉我。最终的解决方案是,实现了两个基类和子类两个类型,通过子类的声明对 Printer.T 进行类型绑定。

最后,我们再来简单对比一下类型擦除的两种方案。如下所示,分别是隐式引用 base 和显式引用 base。其中,Logger 才是 Printer 协议真正的实现者。

具体应用

Codable 源码大量使用了面向协议编程,为了解决泛型协议的存储,其也采用了与上述类似的类型擦除方案。如下所示分别是 Codable 中编解码的核心设计实现,里面涉及到非常多的类,本质上还是在解决泛型擦除。其中,_KeyedEncodingContainerBox_KeyedDecodingContainerBox 中对于 base 的命名有所不同,这里命名成了 concrete。另外,__JSONKeyedEncodingContainer__JSONKeyedDecodingContainer 虽然分别是 KeyedEncodingContainerProcotolKeyedDecodingContainerProtocol 的真正实现者,但是它们内部各自将具体的编码和解码细节转交给了 __JSONEncoder__JSONDecoder

总结

事实上,曾经我也尝试阅读过 Codable 源码,当时对 Swift 类型擦除并不太了解,从而导致我根本读不懂 Codable 的源码在干什么,为什么要有这么多的类进行方法转发。如今,在了解了 Swift 类型擦除之后,Codable 的设计架构一下子就清晰了,后续有时间我们再来探讨一下 Codable 的源码实现。

总而言之,只有深入了解了 Swift 类型擦除后,我们才能领会面向协议编程的精髓以及相关设计理念。