检验一下你的基本功:Type erase

** 这是我的集合系列文章的第一篇,计划从浅入深通过一系列文章将swift的集合相关内容彻底整理清楚,包括集合类相关的第三方代码库,最后自定义一个集合类型,把所有的内容用代码贯穿起来。**

我们之前在实现 SequenceType 协议的时候(Generators VS Sequences VS Collections),使用了 AnyGenerator 这个泛型结构体。在swift的文档中,使用了 “type erase” 这个词来说明 AnyGenerator,我们这次就是掰扯掰扯啥是“type erase”。

为什么需要类型擦除

在swift中,以Any开头的都有类型擦除(type erase)的味道,比方说 AnySequence、AnyForwardIndex、AnyRandomAccessCollection等。当然还有我们最熟悉的Any与AnyObject。

我们首先来看这样一个代码片段:

let seq = AnySequence([1,2,3])

我们用这样方式将数组[1,2,3]转化为一个AnySequence<Int>,可是这种做法会让人很奇怪,因为[1,2,3]自己就是sequence啊,原本就可以进行迭代操作的。那我们为啥还要转成AnySequence呢?这个原因在于[1,2,3]除了是sequence之外,它还是一个数组,有时候我们确实是希望隐藏掉它细节类型(数组)这件事情,而支对外暴露sequence特性。

如果你此时还不是很了解sequence,可以看另外一篇内容Generators VS Sequences VS Collections

比方说,我们考虑一下这样的情况:

let xs = [1,2,3]
let ys = ["A","B","C"]
let zs = zip(xs.reverse(), ys.reverse())

这个时候 zs 的类型是Zip2Sequence<ReverseRandomAccessCollection<Array<Int>>, ReverseRandomAccessCollection<Array<String>>>,那我们会不会去实现一个方法来返回一个这样的类型呢?比方说就像下面这样:

func reverseZip<T,U>(xs: [T], _ ys: [U]) -> Zip2Sequence<ReverseRandomAccessCollection<[T]>, ReverseRandomAccessCollection<[U]>> {
  return zip(xs.reverse(), ys.reverse())
}

我相信你一定不会赞同这样做的,因为这样会完整的暴露了实现的细节,如果有一天,我们把返回值调整成为这个样子:

return zip(xs, ys).reverse()

那么我们的返回值的类型就变成 [(T,U)] ,那么所有调用该方法的地方都要面临着修改。这就是我们过多暴露内部实现所带来的危害。其实此时我们只要返回一个tuples的sequence就可以了,但在很多时候,我们都使用了Array来替代了sequence,我们完全可以使用下面的返回类型:

func reverseZip<T,U>(xs: [T], _ ys: [U]) -> AnySequence<(T,U)> {
    return AnySequence(zip(xs, ys).reverse())
}

通过这样做,我们实现了类型擦除,这样既可以防止实现细节过多的暴露,又可以满足调用者迭代的需求。

AnySequence与Any、AnyObject的类型擦除不同,Any、AnyObject只是把类型隐藏的,我们可以通过 as! AnyObject操作来重新获得原始类型,但AnySequence真的把封装在其内部的数据的类型给隐藏起来了,我们没法获得数据的原始类型了。

让我们开始擦除

让我们看下面这个例子,当然,考验我们基本功的时间也到了.

protocol Animal {
    associatedtype Food
    func feed(food: Food)
}

// Kinds of Food
struct Grass {}
struct Worm {}

struct Cow: Animal {
    func feed(food: Grass) { print("moo") }
}

struct Goat: Animal {
    func feed(food: Grass) { print("bah") }
}

struct Bird: Animal {
    func feed(food: Worm) { print("chirp") }
}

我们首先定义一个协议Animal,并用Cow、Goat、Bird分别实现了Animal,然后我们定义了两种食物,
Grass和Worm,我们知道,牛(Cow)和羊(Goat)吃草(Grass),而鸟(Bird)吃虫(Worm)。

现在需求来了,我们需要给吃草的动物喂草,我们需要这样做:

for animal in grassEaters {
    animal.feed(Grass())
}

所以我们需要定义一个:只吃草类型动物的数组,我们判断一下如果这样操作是否可行:

let grassEaters = [Cow(), Goat()]

结论是否定的,原因就留给你自己补充了。那我们进行一次调整,用显示声明的办法,指明数组的类型为Animal,你认为这样是否可行?

let grassEaters: [Animal] = [Cow(), Goat()]

Cow 和 Goat 都属于 Animal 类型,看上去好像可行啊,实际上我们会收到错误提示:protocol 'Animal' can only be used as a generic constraint because it has Self or associated type requirements。协议毫无疑问是可以用来定义类型的,比方说作为参数类型或返回值类型。问题出在我们定义的Animal是泛型的,也就是说Animal是无法表示表示某一种特定类型的。

let grassEaters: [Animal<Grass>] = [Cow(), Goat()]

这种写法有什么问题呢?和上面的错误相同,另外泛型的占位符不可以是具体类型。

protocol Animal<Food> {
    func feed(food: Food)
}

这种写法的错误在于:协议是没有泛型写法的,只能通过associatedtype关键字来配合实现泛型的效果。为什么这样?可以看另外一篇内容 swift协议为什么不用 <T>语法

那我们到底应该怎样做呢?我们要做的是要实现一个类似AnySequence的东西,我们起名字叫做AnyAnimal,用来擦除一个动物的具体类型,让我们来看一下具体的实现:

struct AnyAnimal<Food>: Animal {
    private let _feed: (Food) -> Void
    init<Base: Animal where Food == Base.Food>(_ base: Base) {
        _feed = base.feed
    }
    func feed(food: Food) { _feed(food) }
}

AnyAnimal是一个泛型的结构体,因为它实现了Animal协议,所以当用它的构造函数接受一个具体动物来创建实例的时候,相当于擦除了动物的具体类型,因为我们得到的是一个 AnyAnimal<Food>类型的实例。相对于我们上面的所有错误示例而言,这个代码片段的优点在于

  • 泛型构造器有效的控制了动物所吃的食物类型 与AnyAnimal指定的食物类型匹配
  • feed方法用调用闭包的方式实现,

现在我们就可以创建grassEaters了。

let grassEaters = [AnyAnimal(Cow()), AnyAnimal(Goat())]

此时grassEaters的类型为 [AnyAnimal<Grass>],在这样的情况下,我们不但创建数组成功,而且还可以获得编译器帮我们提供的类型检查。比方说我们这样创建grassEaters的话:

let mixedEaters = [AnyAnimal(Cow()), AnyAnimal(Bird())]

编译器会报错,因为我们在数据中使用了混合类型,问题出在Bird.Food != Cow.Food.

我们使用这种类型擦除方式可以让我们使用了associatedtype关键字的协议转化为一个泛型类型,这就意味着我们可以把做作为属性或者返回值等我们无法直接使用协议的地方。

本文参考 [http://robnapier.net/erasure]{http://robnapier.net/erasure}

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

推荐阅读更多精彩内容

  • importUIKit classViewController:UITabBarController{ enumD...
    明哥_Young阅读 3,771评论 1 10
  • 作者:Russ Bishop,原文链接,原文日期:2015-01-05译者:靛青K;校对:shanks;定稿:Ce...
    梁杰_numbbbbb阅读 642评论 0 0
  • 作者:Russ Bishop,原文链接,原文日期:2015-01-05译者:靛青K;校对:shanks;定稿:Ce...
    梁杰_numbbbbb阅读 1,240评论 0 5
  • 作者:Russ Bishop,原文链接,原文日期:2015-01-05译者:靛青K;校对:shanks;定稿:CM...
    梁杰_numbbbbb阅读 288评论 0 0
  • 我以为我能洋洋洒洒地写出一大堆什么,可过了半个月,还是无从提笔。 周身氤氲着晕眩的光圈,我不知道该把情绪安放在哪,...
    隔着太阳数月亮阅读 189评论 0 0