Swift 类型擦除
在 《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
。如下所示。
1 | struct AnyPrinter<U>: Printer { |
最终我们基于方法指针隐式地引用了 base
实例。如下所示。
1 | struct AnyPrinter<U>: Printer { |
本文,我们就来探讨一下,在泛型协议中,如何显式地引用
base
实例。
1 | protocol Printer { |
中间类型
上述实现中,在 AnyPrinter
中定义了一个 base
属性,在声明其类型时,无论是声明为 Printer<T>
还是
Printer
,编译器都会报错。为了解决这个问题,我们还是以那句经典名言为指导思想,实现一个包装类型作为
base
属性的类型。
这里我们需要另外定义两个类型,两者是基类和子类的关系,并且都遵循泛型协议
Printer
。至于为什么定义两个类型,我们后面再解释。在 Swift
标准库实现中,经常使用 box
命名中间类型,或者说是盒子类型、包装类型,这里我们同样以
box
进行命名。
Box 基类
如下所示为 box 基类的实现,由于泛型类型
_AnyPrinterBoxBase
遵循了 Printer
泛型协议,类型参数会自动绑定至关联类型。在真正使用时,_AnyPrinterBoxBase
并不会保持抽象,它最终会被绑定到某个特定类型。 1
2
3
4
5
6
7class _AnyPrinterBoxBase<E>: Printer {
typealias T = E
func print(val: E) {
fatalError()
}
}
Box 子类
如下所示为 box 子类的实现,其内部封装了一个实例
var base: Base
,并且将方法传递给了实例。这个
base
实例才是 Printer
协议真正的实现者。在 _PrinterBox
类型声明的第一行中,其自动将 Base.T
(Printer
协议的关联类型)绑定为 _AnyPrinterBoxBase.T
(_AnyPrinterBoxBase
的类型参数) 。此时,我们也无需再在
_PrinterBox
内部通过 typealias T == xxx
的方式手动进行类型绑定。
1 | class _PrinterBox<Base: Printer>: _AnyPrinterBoxBase<Base.T> { |
类型擦除
在实现了中间类型后,我们再来修改类型擦除包装器
AnyPrinter
的内部实现。具体如下所示,由于我们使用中间类型
box 对 base
进行了封装,所以这里我们需要将
AnyPrinter
中的 base
的命名修改为
_box
。当我们调用 print
方法时,其内部会将
print
方法转发至 _box
,而 _box
内部又会将 print
转发至 base
这个真正的实现者。
1 | struct AnyPrinter<T>: Printer { |
现在,我们再来看前文留下的问题:为什么中间层需要定义基类和子类两个类型。事实上一开始,我尝试只定义一个
box 类型 _PrinterBox
,如下所示,但是编译器会报错:
1
2
3
4
5
6
7
8
9
10
11
12
13class _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 == T
对 Base
类型进行了约束,但是却并没有将 Printer.T
绑定至
Base
类型。不过奇怪的是,我加了
typealias T = Base
也不管用。如果有人知道原因,可以留言告诉我。最终的解决方案是,实现了两个基类和子类两个类型,通过子类的声明对
Printer.T
进行类型绑定。
最后,我们再来简单对比一下类型擦除的两种方案。如下所示,分别是隐式引用
base 和显式引用 base。其中,Logger
才是
Printer
协议真正的实现者。
具体应用
Codable
源码大量使用了面向协议编程,为了解决泛型协议的存储,其也采用了与上述类似的类型擦除方案。如下所示分别是
Codable
中编解码的核心设计实现,里面涉及到非常多的类,本质上还是在解决泛型擦除。其中,_KeyedEncodingContainerBox
和 _KeyedDecodingContainerBox
中对于 base
的命名有所不同,这里命名成了
concrete
。另外,__JSONKeyedEncodingContainer
和 __JSONKeyedDecodingContainer
虽然分别是
KeyedEncodingContainerProcotol
和
KeyedDecodingContainerProtocol
的真正实现者,但是它们内部各自将具体的编码和解码细节转交给了
__JSONEncoder
和 __JSONDecoder
。
总结
事实上,曾经我也尝试阅读过 Codable 源码,当时对 Swift 类型擦除并不太了解,从而导致我根本读不懂 Codable 的源码在干什么,为什么要有这么多的类进行方法转发。如今,在了解了 Swift 类型擦除之后,Codable 的设计架构一下子就清晰了,后续有时间我们再来探讨一下 Codable 的源码实现。
总而言之,只有深入了解了 Swift 类型擦除后,我们才能领会面向协议编程的精髓以及相关设计理念。