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。如下所示。

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 属性的类型

image

这里我们需要另外定义两个类型,两者是基类和子类的关系,并且都遵循泛型协议 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 协议真正的实现者。

image
image

具体应用

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

image
image

总结

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,188评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,464评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,562评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,893评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,917评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,708评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,430评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,342评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,801评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,976评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,115评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,804评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,458评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,008评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,135评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,365评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,055评论 2 355

推荐阅读更多精彩内容