[译]Swift 中的类型擦除

找图好辛苦

你可能听过这个术语 :类型擦除。甚至你也用过标准库中的类型擦除(AnySequence)。但是具体什么是类型擦除, 我们怎么才能实现类型擦除呢?这篇文章就是介绍这件事情的。

在日常的开发中, 总有想要把某个类或者是某些实现细节对其他模块隐藏起来, 不然总会感觉这些类在项目里到处都是。或者想要实现两个不同类之间的互相转换。类型擦除就是一个移除某个类的类型标准, 将其变得更加通用的过程。

到这里很自然的就会想到协议或者是提取抽象的父类来做这件事情。协议或者父类 就可以看作是一种实现类型擦除的方式。举个例子:

NSString 在标准库中我们是没办法得到 NSString 的实例的,我们得到的所有的 NSString 对象其实都是标准库中 NSString 的私有子类。这些私有类型对外界可以说是完全隐藏起来了的, 同时可以是用 NSString 的 API 来使用这些实例。所有的子类我们在使用的时候都不需要知道他们具体是什么, 也就不需要考虑他们具体的类型信息了。

在处理 Swift 中的泛型和有关联类型的协议的时候, 就需要一些更高级的东西了。Swift 不允许把协议当作类来使用。如果你想要写一个接受一个 Int 类型的序列的方法。这么写是不对的:

func f(seq: Sequence<Int>) {...}

// Compile error: Cannot specialize non-generic type 'Sequence'

这种情况下, 我们应该考虑使用的是泛型:

func f<S: Sequence>(seq: S) where S.Element == Int { ... }

这样写就可以了。但是, 还是有一些情况是比较麻烦的比如说: 我们无法使用这样的代码来表达返回值类型或者是属性

func g<S: Sequence>() -> S where S.Element == Int { ... }

这么写并不会是我们想要的那种结果。在这行代码中,我们想要的是返回一个满足条件的类的实例,但是这行代码会允许调用者去选择他想要的具体的类型, 然后 g 这个方法去提供合适的值。

protocol Fork {
    associatedtype E
    func call() -> E
}

struct Dog: Fork {
    typealias E = String
    func call() -> String {
        return "🐶"
    }
}

struct Cat: Fork {
    typealias E = Int
    
    func call() -> Int {
        return 1
    }
}

func g<S: Fork>() -> S where S.E == String {
    return Dog() as! S
}

// 在这里可以看出来。g 这个函数具体返回什么东西是在调用的时候决定的。就是说要想正确的使用 g 这个函数必须使用  `let dog: Dog = g()`  这样的代码
let dog: Dog = g()
dog.call()

// error
let dog = g()
let cat: Cat = g()

Swift 提供了 AnySequence 这个类来解决这个问题。AnySequence 包装了任意的 Sequence 并把他的类型信息给隐藏起来了。然后通过 AnySequence 来代替这个。有了 AnySequence 我们可以这样来写上面的 fg 方法。

func f(seq: AnySequence<Int>) { ... }
func g() -> AnySequence<Int> { ... }

这么一来, 泛型没有了, 而且所有具体的类型信息都被隐藏起来了。使用 AnySequence 增加了一点点的复杂性和运行成本,但是代码却更干净了。

Swift 标准库中有很多这样的类型, 比如 AnyCollection, AnyHashable, AnyIndex 等。 在代码中你可以自己定义一些泛型或者协议, 或者直接使用这些特性来简化代码。

基于类的擦除

我们需要在不公开类型信息的情况下从多个类型中包装出来一些公共的功能。这很自然就能想到抽象父类。事实上我们确实可以通过抽象父类来实现类型擦除。父类暴露 API 出来,子类根据具体的类型信息来做具体的实现。我们来看看怎么自己实现一个类似 AnySequence 的东西。

class MAnySequence<Element>: Sequence {

这个类需要实现 iterator 类型作为 makeIterator 的返回类型。我们必须要做两次类型擦除来隐藏底层的序列类型以及迭代器的类型。这种内在的迭代器类型遵守了 IteratorProtocol 协议并且在 next() 方法中使用 fatalError 来抛出异常。Swift 本身是不支持抽象类的, 所以这就足够了:

    class Iterator: IteratorProtocol {
        func next() -> Element? {
            fatalError("Must override next()")
        }
    }

ManySequencemakeIterator 方法的实现也差不多, 使用 fatalError 来抛出异常。 这个错误用来提示子类来实现这个功能:

    func makeIterator() -> Iterator {
        fatalError("Must override makeIterator()")
    }

这就是基于类的类型擦除需要的公共 API。私有的实现需要去子类化这个类。这公共类被元素的类型参数化, 但是私有的实现却在这个类型当中:

private class MAnySequenceImpl<Seq: Sequence>: MAnySequence<Seq.Element> {

这个类需要内部的子类来实现上面提到的两个方法:

class IteratorImpl: Iterator {

这一步包装了这个序列的迭代器的类型

    class IteratorImpl: Iterator {
        var wrapped: Seq.Iterator
        
        init(_ wrapped: Seq.Iterator) {
            self.wrapped = wrapped
        }
    }

这一步实现了 next 方法。 实际上是调用它包装的序列的迭代器的 next 方法.

        override func next() -> Element? {
            return wrapped.next()
        }

相似的, MAnySequenceImpl 是 sequence 的包装。

    var seq: Seq
    
    init(_ seq: Seq) {
        self.seq = seq
    }

这一步实现了 makeIterator 方法。从包装的序列中去获取迭代去对象, 然后把这个迭代器对象包装给 IteratorImpl

    override func makeIterator() -> IteratorImpl {
        return IteratorImpl(seq.makeIterator())
    }

还需要一点: 使用 MAnySequence 来初始化一个 MAnySequenceImpl,但是返回值还是标记成 MAnySequence 类型。

extension MAnySequence {
    static func make<Seq: Sequence>(_ seq: Seq) -> MAnySequence<Element> where Seq.Element == Element {
        return MAnySequenceImpl<Seq>(seq)
    }
}

我们来用一下这个 MAnySequence:

func printInts(_ seq: MAnySequence<Int>) {
    for elt in seq {
        print(elt)
    }
}

let array = [1, 2, 3, 4, 5]
printInts(MAnySequence.make(array))
printInts(MAnySequence.make(array[1 ..< 4]))

基于函数的擦除

我们希望公开多个类型的功能而不公开这些类型。很自然的方法是储存那些签名只涉及到我们想要公开的类型的函数。函数的主体可以在底层信息已知的上下文中创建。

我们来看看 MAnySequence 要怎么来实现呢?更上面的内容差不多。只是这次因为我们不需要继承而且他只是一个容器,所以我们用 Struct 来实现。

还是声明一个 Struct

struct MAnySequence<Element>: Sequence {

跟上面一样, 实现 Sequence 协议需要有一个迭代器(Iterator)来作为返回值。这个东西也是一个 struct 它有一个储存属性, 这个储存属性是一个不接受参数, 返回一个Element? 的函数。 他是 IteratorProtocol 这个协议要求的

    struct Iterator: IteratorProtocol {
        let _next: () -> Element?
        
        func next() -> Element? {
            return _next()
        }
    }

MAnySequence 跟这个也相似。他包含了一个返回 Iterator 的函数的储存属性。 Sequence 通过调用这个函数来实现。

    let _makeIterator: () -> Iterator
    
    func makeIterator() -> Iterator {
        return _makeIterator()
    }

MAnySequenceinit 方法是最重要的地方。他接受任意的 Sequence 作为参数(Sequence<Int>Sequence<String>):

init<Seq: Sequence>(_ seq: Seq) where Seq.Element == Element {

然后需要把这个 Sequence 需要的功能包装在这个函数中:

        _makeIterator = {

再然后我们需要在这里做一个迭代器 Sequence 正好有这个东西:

var iterator = seq.makeIterator()

最后我们把这个迭代器包装给 MAnySequence。 他的 _next 函数就能调用到 iteratornext 函数了:

            return Iterator(_next: { iterator.next() })
        }
    }
}

下面看这个 MAnySequence 是怎么用的:

func printInts(_ seq: MAnySequence<Int>) {
    for elt in seq {
        print(elt)
    }
}

let array = [1, 2, 3, 4, 5]
printInts(MAnySequence(array))
printInts(MAnySequence(array[1 ..< 4]))

搞定!

这种基于函数的擦除方法在处理需要把一小部分功能作为更大类型的一部分来包装的时候非常有效, 这样做就不需要有单独的类来擦除其他类的类型信息。

比如说,我们需要写一些能在特定几个集合类型上面使用的代码:

class GenericDataSource<Element> {
    let count: () -> Int
    
    let getElement: (Int) -> Element
    
    init<C: Collection>(_ c: C) where C.Element == Element, C.Index == Int {
        count = { Int(c.count) }
        getElement = { c[$0 - c.startIndex]}
    }
}

这样, GenericDataSource 中的其他代码就能够直接使用 count()getElement() 两个方法来操作传入的collection 了。并且这个集合类型不会污染 GenericDataSource 的泛型参数。

总结

类型擦除是个非常有用的技术。他被用来阻止泛型对代码的侵入, 也能够让接口更加的简单。通过将底层的类型信息包装起来, 将 API 和具体的功能分开。使用静态的公有类型或者将 API 包装进函数都能够做到类型擦除。基于函数做类型擦除对那种只需要几个功能的简单情况尤其有用。

Swift 标准库提供了一些可以直接使用的类型擦除。AnySequenceSequence 的包装, 从名字可以看出来, 他允许你在不知道具体类型的情况下迭代遍历某个序列。AnyIterator 是他的好朋友, 它提供了一个类型已经被擦除掉的迭代器。AnyHashable 包装了类型擦除掉了的 Hashable 类型。Swift 中还有一些基于集合类型的协议。在文档中搜索 “Any” 就可以看到。标准库中的 Codable 也有用到了类型擦除: KeyedEncodingContainerKeyedDecodingContainer 都是对应协议类型擦除的包装。他们用来在不知道具体类型信息的情况下实现 encode 还有 decode。

最后

前几天看到 MikeAsh 最新的 Friday Q&A Type Erasure in Swift。想趁着最近没什么事情翻译一下的。结果最近一直沉迷吃鸡, 没有时间去做这件事情。所以...

致读者

前段时间的风波过后, 很多小伙伴都以及离开了简书这个平台。我自己也在 掘金 上开始了新的旅程。但是在早前的学习过程中,查阅过大量在简书上面的文章,甚至有段时间可以说是面向简书编程。可以说在技术这条路上,简书帮助了我很多。现在他不大欢迎程序员了。所以 之前的文章我不会删除,或者迁移到其他地方去,之后的文章全部都会在博客以外的掘金同步。但是不一定全部都会发在简书上。 每次都要在各个平台都要去弄,真的很烦。

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