Swift之集合

前言

集合(Collection)是建立在序列(sequence)上层的类型,它添加了可重复遍历元素和根据下标访问元素的功能。

为了具体说明Swift中的集合实现原理。我们会实现一个自己的集合。可能Swift标准库中没有实现的最有用的容器类型就是队列(queue)了。Swift的数组可以很容易的拿来当栈使用——append就是压栈,removeLast就是弹栈。但把数组当队列用就不合适了。你可以把appendremoveAtIndex(0)方法结合起来用,但移除队列中的元素(不是最后一个)是一个O(n)的操作。因为数组元素在内存中连续存储,移除一个元素意味着后面的元素都要向前移动来弥补被移走的元素留下的空缺。

设计队列的协议

在我们实现队列之前,我们最好先确定一下我们的队列需要什么功能。一个比较好的方法是先定义一个描述队列的协议:


尽管这是一个非常简单的队列协议的定义,他依然提供了很多队列的定义。比如,队列被定义为一个范性容器,通过类型别名Element,他可以容纳任何类型的元素,它对元素类型没有要求,只要保证是某一个具体的类型即可。

需要强调一点对于enqueuedequeue方法的时间复杂度没有明确要求。我们固然可以规定这两个方法在O(1)的常量时间内完成,而且这样会给用户留下高性能的印象,但同时也会不再适合某些数据结构。比如优先队列(priority queue)的入队方法需要O(log n)的时间复杂度。

这个协议没有提供peek方法允许用户在获取队首元素的同时不用dequeue。这意味着实现这个协议的队列不具备这一特性(比如操作系统和网络的队列接口就没有peek方法)。它也没有这两个操作是否是线程安全的,以及队列是否是一个集合类型(尽管我们将要实现的版本确实如此)。

这个协议甚至都没有指定队列是否是先入先出,也就是说可能是后入先出的。如果把apend当做入队,isEmpty配合removeLast实现队列功能,我们还可以让数组也实现这个协议

数组和可选类型

不过这个协议还是指定了一些信息的。比如与removeLast不同的是,dequeue函数返回的是可选类型的值。如果队列为空,dequeue会返回nil,如果数组为空,调用removeLast会导致程序退出。

这种风格很大程度上基于个人偏好。Swift的数组更倾向于在调用方法前由用户考虑数组当前的状态,再决定不能调用方法。最明显的一个例子是数组的下标,你最好确保数组有不少于十个元素,然后才去获取下标为9的数组元素。调用空数组的removeLast方法会导致程序退出。

Swift这么做主要考虑到数组切片的使用。在Swift中,计算下标其实是很罕见的:

遍历结合元素可以用:for x in collection

遍历集合切片可以用:for idx in collection.indices

获得所有元素和下标:for (num, element) in collection.enumerate()

找到某个指定的元素:if let idx = collection.indexOf({ someMatchingLogin($0) })

变换集合中的所有元素:array.map {someTransformation($0)}

获取符合某个要求的元素:collection.filter{someCriteria($0)}

一旦你发现你用到了形如:


赶紧挺犀利想想你是否真的需要这样写。有时候你确实需要这么做,但是大多数时候你可以用一种更清楚易读的方法来实现同样的功能。手动管理数组下标在我看来极容易导致bug,所以最好不要这么做。如果必须这么做的话,我们可以用一些可以重用的范型函数来代替这种写法。这样就可以把经过精心测试的某一小段用于计算下标的代码封装起来。

有时候非用下标不可,那么久需要极其小心处理下标计算背后的逻辑。所以解封通过下标脚本获取的值,往往是多此一举的——这意味着你其实不相信你的代码。但有时候正是因为你过于相信自己的代码,所以有可能直接强制解封(force-unwrapping)获取到的值,这么做既让人反感,同时也不是什么好习惯。常在河边走哪有不湿鞋。如果你的代码到处都是强制解包,迟到要掉进坑里。所以不要让这种不好的风格成为习惯。

removeLast函数要稍微复杂些。如果把数组当做栈来用,只要数组不是空的,你就可能希望数组最后一个元素出栈:


但是如果让dequeue函数的范围值也是可选类型,你就可以把代码缩短到一行,同时也不会出什么错误:


这么做(为了获取简洁性和安全性而在明知集合不为空时候还要进行一层封装)是否利大于弊,就取决于你自己了。

队列的实现

现在我们定义好了队列,是时候实现它了。

下面是一个非常简单的队列代码,只包含了enqueuedequeue函数的实现。

因为我们已经把队列的范型占位符定义为Element了,也就是实际需要的类型别名,所以就没有必要再定义类型别名了。因为占位符只是你随便取的一个名字,如果我们让他叫Foo,我们可以再加一句typealias Element = Foo或者干脆就让Swift的类型推导功能根据enqueuedequeue的返回值类型自己推理。


这个实现其实是用两个栈(也就是Swift中的数组)来模拟队列。当元素入队时,他们进入右侧的栈。当元素出队时候,从左侧的栈出来。当左侧的栈为空时候,就颠倒右侧栈中所有元素并放入左侧栈中。

这就是为什么单个操作时间不定,但多次操作的平均时间是常量。通过同样的分析,我们也可以知道为什么向数组添加元素,也是花费常数级时间。当数组容量用尽时,系统会分配一块更大的空间然后把现有元素都拷贝过去。因为每次充分配空间都会将之前的容量翻倍,所以你可以套用同样的分析:“每次添加元素拿到一个令牌,扩容复制时花费一个令牌,花费总不会超过收入”。

实现CollectionType协议

现在我们有一个可以调用enqueuedequeue方法的容器,为了让他真正成为一个集合,队列需要实现CollectionType协议:


CollctionType协议定义了一个Index别名,不过和Element一样可以用方法和属性定义推导出来。

通过这几行代码,队列实现了CollectionType(自然也就实现了SequenceType协议)。队列现在就有超过40种方法和属性由那俩个协议进行管理了。下面给出一段队列的使用:


我们模仿了数组的习惯,对于无效的下标不会返回可选类型的值而是直接报错。这里调用Queue.first返回了一个即将出队的元素,它的作用类似于之前所说的peek

不过先别急着高兴,SequenceType协议要求我们提供一个generate方法,来创建生成器,我们的代码中没有这么做,所以它能够通过编译么?

在Swift2.0以前,我们确实需要实现generate()方法——通过使用IndexingGenerator辅助结构体来创建一个生成器。这个生成器可以通过下标脚本从头到尾遍历元素:


考虑到队列的逻辑,我们希望反转所有元素并放到左侧栈中,因为它们以后出队时也得放到左侧栈中。所以不如直接放在左侧栈,省去一次元素复制的操作。

现在我们可以很容易的用字面量创建队列了:


需要强调一点的是,Swift中的字面量和类型是不一样的。这里的[1, 2, 3]不是数组,而是数组字面量。它可以用于创建任何一个实现了ArrayLiteralConvertible协议的类型的实例对象。这个数组字面量内部含有整数字面量,可以用于创建任何实现了IntegerLiteralConvertible协议的类型。

这些字面量是有默认类型的,如果你没有指定它的类型,Swift会把它当做默认的类型。比如数组字面量会默认生成一个数组,整数字面量对应的默认类型是Int,字符串字面量对应的类型是String。但默认类型只在你不指明变量类型时候才会生效,比如之前我们把队列声明为整数的队列,但我们也可以把它定义为别的类型的整数:


字面量的类型一般可以通过上下文推断出来。比如下面就是一个接受字面量作为参数的函数:


实现RangeReplaceableCollectionType协议

下一个需要实现的协议是RangeReplaceableCollectionType,这个协议有三个要求:

一个空的初始化方法。它通过一个函数创建新的空集合作为占位符,这些空集合都是相同的类型,这在范型编程中很有用。

一个空的初始化方法。它通过一个函数创建新的空集合作为占位符,这些空集合都是相同的类型,这在范型编程中很有用。

一个replaceRange函数。这个函数用一个新的集合替换原来集合中指定的范围。RangeReplaceableCollectionType协议充分展示了协议拓展的强大。你只要实现一个非常灵活的replaceRange方法,立刻就可以使用由此得到的一系列方法:

appendappendContentsOf方法——在末尾添加一个或多个新元素(相当于替换了endIndex..


.

上图为2017年最新的视频教程资料,搜索2352149755加我好友私聊我上传视频教程,有什么不懂的也可以来私聊问我。

不定时更新中。

如果你能明白这些视频资料的好差,那么你也算是入行了,底层和中高层就是这一步之差。

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

推荐阅读更多精彩内容

  • Swift中提供了三种主要的集合类型,Array,Sets,Dictionary Array Swift数组中的值...
    lion_xion阅读 487评论 0 3
  • 数组定义遍历 增/删/改 合并 字典 定义 遍历 增/删/改 合并 一 数组 定义 遍历 增删改 合并 二 字典 ...
    freemanIT阅读 259评论 0 0
  • 136.泛型 泛型代码让你可以写出灵活,可重用的函数和类型,它们可以使用任何类型,受你定义的需求的约束。你可以写出...
    无沣阅读 1,450评论 0 4
  • 前言 3月27号苹果发布了Swift3.1,官方教程也更新到了3.1,查看更新记录发现更新的内容对之前的文章并没有...
    BoomLee阅读 3,126评论 0 4
  • 绘画,在百度上的词义,是指用笔、板刷、刀、墨、颜料等工具材料,在纸、纺织物、木板、墙壁等平面上塑造形象的艺术形式。...
    影子倒了阅读 2,097评论 9 15