类型擦除
近期较忙已经好久没写文章了! 有点懒惰了呢! 今天来跟小伙伴们一起探讨类型擦除,正好也复习一下!
那么什么是类型擦除呢?
moand转换,通过技术手段(通常是包装器),将具体类型的类型信息擦除掉了! 只将类型的抽象信息,通常指的是类型尊从的协议、接口、或基类暴露出来。
假如我们在 AnyGenerator 中定义一个 base 属性用于显式引用实例。当我们将 base 声明为 GenericProtocol,编译器报错:Protocol 'GenericProtocol' can only be used as a generic constraint because it has Self or associated type requirements。
代码如下:
protocol GenericProtocol {
associatedtype AbstractType
var value: AbstractType { get set}
func generate(val: AbstractType) -> AbstractType
}
struct AnyGenerator<T>: GenericProtocol {
var value: T
// error: Protocol 'GenericProtocol' can only be used as a generic constraint because it has Self or associated type requirements
var _base: GenericProtocol
init<Base: GenericProtocol>(_ base: Base) where Base.AbstractType == T {
_base = base
}
func generate(val: T) -> T { val }
}
尝试基于方法指针隐式地引用了 base 实例,如下所示:
struct AnyGenerator<T>: GenericProtocol {
var value: T
private let _generate: ((T) -> T)?
init<Base: GenericProtocol>(_ base: Base) where Base.AbstractType == T {
self.value = base.value
_generate = base.generate
}
func generate(val: T) -> T {
guard let call = _generate else {
return T.self as! T
}
return call(T.self as! T)
}
}
虽说, 一眼看起来好像是可以了! 但总不能把 base 中的函数或属性都这样写一遍吧!
客官别着急, 这样肯定是不行!
那么我们应该如何基于类型擦除技术解决 Swift 泛型协议的存储问题?
答: 可以通过定义一个类型擦除包装器来瞒过编译器,解决泛型协议 GenericProtocol 的存储问题!
接下来我们就来探讨一下,在泛型协议中,如何显式地引用 base 实例。
中间类型
在上述代码中,在 AnyGenerator 中定义了一个 base 属性,在声明其GenericProtocol 类型时,编译器会报错。为了解决这个问题,可以通过实现一个包装类型作为 base 属性的类型。
首先定义两个类型,两者是基类和子类的关系,并且都遵循泛型协议 GenericProtocol。至于为什么定义两个类型,我们后面再解释。
Base基类代码实现如下,由于泛型类型 GeneratorBoxBase 遵循了 GenericProtocol 泛型协议,类型参数会自动绑定至关联类型。在真正使用时,GeneratorBoxBase 并不会保持抽象,它最终会被绑定到某个特定类型。
class GeneratorBoxBase<T>: GenericProtocol {
var value: T?
func getType(val: T) {
value = val
}
func generate(val: T?) -> T? { val }
}
子类代码实现如下,其内部封装了一个实例 var _base: Base,并且将方法传递给了实例。这个 base 实例才是 GenericProtocol 协议真正的实现者。在 GeneratorBox 类型声明中,会将其Base.AbstractType (GenericProtocol 协议的关联类型)绑定为 GeneratorBoxBase中的泛型类型,也就是 <T>。
此时,我们也就不需要在 GeneratorBox 内部通过 Base.AbstractType == T 的方式进行类型校验。
class GeneratorBox<Base: GenericProtocol>: GeneratorBoxBase<Base.AbstractType> {
private(set) var _base: Base?
init(base: Base) {
_base = base
}
}
类型擦除
在实现了中间类型后,接下来我们开始使用。首先 struct People 与 class Person各自创建一个,由于我们使用中间类型 GeneratorBox 对 base 进行了封装,可以直接将其对象添加到Box中,进行管理以及使用
具体代码如下所示:
struct People<T>: GenericProtocol {
var value: T
mutating func getType(val: T) {
self.value = val
print("People : \(val)")
}
func generate(val: T) -> T {
val
}
}
class Person<T>: GenericProtocol {
var value: T?
func getType(val: T) {
value = val
}
func generate(val: T?) -> T? { val }
}
var people = People(value: 1)
people.getType(val: 18)
let printPeople = GeneratorBox(base: people)
print(printPeople._base?.value)
let person = Person<String>()
person.getType(val: "demo")
let printString = GeneratorBox(base: person)
print(printString._base?.value!)
现在,我们再来看前文留下的问题:为什么中间层需要定义基类和子类两个类型?
我们再来看一下只定义一个 中间 类型 GeneratorBox,代码如下所示
struct GeneratorBox <Base: GenericProtocol>: GenericProtocol {
var value: Base.AbstractType
var _base: Base
func generate(val: Base.AbstractType) -> Base.AbstractType {
val
}
}
struct People<T>: GenericProtocol {
var value: T
mutating func getType(val: T) {
self.value = val
print("People : \(val)")
}
func generate(val: T) -> T { val }
}
let p = People(value: 1)
let box = GeneratorBox(value: 20, _base: p)
这么一看也没毛病呀! 但在实际开发中,对于属性我们有时也会定义为可选类型,我们再来看看, 如果属性为可选类型会发生什么?
// error: Type 'People<T>' does not conform to protocol 'GenericProtocol'
struct People<T>: GenericProtocol {
var value: T?
mutating func getType(val: T) {
self.value = val
print("People : \(val)")
}
func generate(val: T) -> T {
val
}
}
var number: Int? = 1
let p = People(value: number)
// error: Cannot convert value of type 'Int?' to expected argument type 'People<Int>.AbstractType'
let box = GeneratorBoxBase(value: number, _base: p)
果然还是会报错的呀! 编译器给你安排的明明白白的!
所以,当我们遇到不能直接使用泛型协议进行变量定义的时候,我们可以利用一些具体实现的通用泛型类,去包装具体实现了该泛型协议的类。
总而言之,只有深入了解了 Swift 类型擦除后,我们才能领会面向协议编程的精髓以及相关设计理念。