转载:http://www.cocoachina.com/ios/20151218/14716.html

最近翻译完了《Advanced Swift》中文版的“集合”章节。书的质量非常高,讲解非常细致。但不可避免的导致篇幅有点长,有些前面的知识点看到后面无法串联起来。同时由于偏重于讲解,所以个人感觉总结还不够,比如我们可以考虑这几个问题:

  • 数组类型(_ArrayType)、集合(Collection)、序列(Sequence)、生成器(Generator)、元素(Element)、下标(Index),这些类型(协议)各自的作用。

  • 数组是如何利用上面这些类实现各种方法的。

  • map、reduce、filter等方法的作用是什么,他们是怎么实现的。

  • 只有数组有上面这些方法‘么,如果不是,什么样的类型才有这些方法。

  • 如果实现一个自定义的集合类型,应该怎么做。

而这些问题恰恰是过去我们没有重视或根本无法找到答案的问题。因为在OC中,由于NSArray封装的非常好,而且在单纯的iOS开发中对数组的理解不用非常深入,所以我们很少深究数组背后的实现原理。

其实答案就遍布在《Advanced Swift》中文版的“集合”章节的各篇文章中。本文会通过分析Swift中数组的实现,回答上述问题并尝试着建立完整的知识体系。由于篇幅所限,本文不会给出非常详细的源码,而是做总结性的提炼。

参考文章

  • Advanced Swift——数组可变性:主要介绍数组的值语义

  • Advanced Swift——数组变换: 主要介绍map、reduce、filter等方法的使用和原理

  • Advanced Swift——字典与集合:主要介绍字典与集合类型的使用

  • Advanced Swift——集合协议:主要介绍数组相关的三种协议

  • Advanced Swift——集合:通过实现一个队列,介绍CollectionType类型的使用

  • Advanced Swift——下标:实现自定义链表,介绍数组下标的相关知识

相关类型简介

我们从零开始,根据集合最本质的特点开始思考,逐步抽象。

  • 元素(Element)

对于任何一种集合来说,它本质上是一种数据结构,也就是用来存放数据的。我们在各种代码中见到的Element表示的就是最底层数据的类型别名。因为对于集合这种范型类型来说,它不关心具体存放的数据的类型,所以就用通用的Element来代替,比如我们写这样的代码:

1
letarray:Array=[1,2,3]

这就相当于告诉数组,Element现在具体变为Int类型了。

  • 生成器(Generator)

对于集合来说,它往往还需要提供遍历的功能。那怎么把它设计为可遍历的呢?不要指望for循环,要知道目前我们什么都没有,没有数组的概念,更没有下标的概念。有一句名言说:“任何编程问题都可以通过增加一个中间层来解决”。于是,我们抽象出一个Generator(生成器)的概念。我们把它声明成一个协议,任何实现这个协议的类都要提供一个next方法,返回集合中的下一个元素:

1
2
3
4
protocolGeneratorType{
typealiasElement
mutatingfuncnext()->Element?
}

可以想象的是这样一来,实现了GeneratorType协议的类,就可以隐藏具体元素的信息,通过不断调用next方法就可以获得所有元素了。比如你可以用一个生成器,不断生成斐波那契数列的下一个数字。

总的来说,生成器(Generator)允许我们遍历所有的元素。

  • 序列(Sequence)

生成器不断生成下一个元素,但是已经生成过的元素就无法再获取到了。比如在斐波那契数列中通过3和5得到了8,那么这个生成器永远也不会再生成元素3了,下一个生成的元素是13。也就是说对于生成器来说,已经访问过的元素就无法再次获取到。

然而对于集合来说,它所包含的元素总是客观存在的。为了能够多次遍历集合,我们抽象出了序列(Sequence)的概念。Sequence封装了Generator对象的创建过程:

protocolSequenceType{
typealiasGenerator:GeneratorType
funcgenerate()->Generator
序列(Sequence)相比于单纯的Generator,使反复遍历集合中的元素成为可能。这时候,我们已经可以通过for循环而不是Generator来遍历所有元素。
  • 集合(Collection)

对比一下现有的Sequence和数组,会发现它还欠缺一个特性——下标。

回顾一下Generator和Sequence,它们只是实现了集合的遍历,但没有指定怎么遍历。也就是说,只要Generator设计“得当”,即使是1和2这两个元素,我们也可以不断遍历:“1的next是2,2的next是1”。这种情况显然不符合我们对数组的认识。归根结底,还是Sequence中无法确定元素的位置,也就无法确保不遍历到已经访问过的元素。

基于这种考虑,我们抽象出集合(Collection)的概念。在集合中,每个元素都有确切的位置,因此集合有明确的开始位置和结束位置。给定一个位置,就可以找到这个位置上的元素。Collection在Sequence的基础上实现了Indexable协议

4
5
publicprotocolCollectionType:Indexable,SequenceType{
public var startIndex:Self.Index{get}
endindex:Self.Index{get}
publicsubscript(position:Self.Index)->Self._Element{get}
当然,CollectionType中的内容远远不止这些。这里列出的仅仅是为了表示CollectionType的特性。
  • 下标(Index)

虽然我们在使用数组的时候,元素下标总是从0开始,并且逐个递增。但下标不必是从0开始递增的整数。比如a、b、c……也可以作为下标。下标类型的关键在于能根据某一个下标推断出下一个下标是什么,比如对于整数来说下一个下标就是当前下标值加1。下标类型的协议如下:

5
6
7
publicprotocolForwardindexType:_Incrementable{
///....
}
publicprotocol_Incrementable:Equatable{
publicfuncsuccessor()->Self
对于下标类型来说,它们必须实现successor()方法,同时也得是Equatable的,否则怎么知道某个位置上的元素已经被访问过了呢。

数组简介

有了以上基本知识做铺垫,接下来就可以研究一下Swift中数组是怎么实现的了。

  • 基本定义

我们可能早已体会到Swift数组的强大,它的下标脚本不仅可以读取元素,还可以直接修改元素。它可以通过字面量初始化,还可以调用appendContentsOf方法从别的数组中添加元素。甚至于我们可能都没有仔细考虑过为什么可以用for number in array这样的语法。

首先,我们需要明确一个概念:数组丰富强大的特性绝不是由Array这一个类型可以提供的。实际上,这主要是协议的功劳。用OC写iOS时,协议几乎就是代理的代名词(可能是我对OC不甚精通,目光短浅),但毋庸置疑的是在Swift中,协议的功能被空前的强化。数组通过实现协议、以及协议的嵌套、拓展功能,具有了很多特性。数组的背后交织着错综复杂的协议、方法和拓展。

下面是数组的定义:

publicstructArray:CollectionType,MutableCollectionType,_DestructorSafeContainer{
startIndex:Int{get}
endindex:Int{get}
publicsubscript(index:Int)->Element
publicsubscript(subRange:Range)->ArraySlice}

数组是一个结构体,它实现了三个协议,有两个变量和两个下标脚本。除此以外,数组还有大量的拓展。

  • 数组拓展

数组的大量功能在拓展中完成,由于拓展很多,我就不列出完整代码,只是做一个整理。数组一共拓展了四类协议:

ArrayLiteralConvertible: 这个协议是为了支持这样的语法的:let a: Array= [1,3]。实现协议很简单,只要提供一个自定义方法即可:

publicinit(arrayLiteralelements:Element...)

_Reflectable:这个协议用于处理反射相关的内容,这里不做详解

customstringconvertible和CustomDebugStringConvertible:这两个是方便我们调试的协议,与数组自身的功能无关。

_ArrayType:这是数组最关键的部分。在实现这个协议之前,数组还只是一个普通的集合类型,它仅仅是拥有下标,而且可以重复遍历所有元素而已。而通过实现_ArrayType协议,它可以在指定位置(下标)添加或移除一个或多个元素,它还有了自己的count属性。

这一点也许有些颠覆我们此前的认识。一个集合类型,是不能添加或删除元素的。数组通过实现了_ArrayType协议提供了这样的功能。但这也很容易理解,因为集合的本质还是在于元素的收集,而非考虑如何改变这些元素。

_ArrayType协议还给了我们一个非常重要的启示。比如说我想实现自己的数据结构——栈,那么就应该实现对应的_StackType协议。这种协议要充分考虑数据结构自身的特点,从而提供对应的方法。比如我们不可能向栈的某个特定位置添加若干个元素(只能向栈顶添加一个)。所以_StackType协议中不会定义append、appendContentsOf这样的方法,而是应该定义pop和push方法。

SequenceType

接下来的任务是搞清楚ColectionType的原理了。不过在此之前,我们先来看看SequenceType的一些细节,毕竟CollectionType协议是继承了SequenceType协议的。

在有了Generator之后,我们已经可以在while循环中用Generator的next方法遍历所有元素了。之前也说过,SequenceType使对元素的多次遍历成为可能。

注意,仅仅是成为可能而已。如果遍历的细节由Generator控制,那么多次遍历是没有问题的。在极个别情况下,但如果遍历的细节依赖于SequenceType自身的某个属性,而且这个属性会发生变化,那么就不能多次遍历所有元素了。

SequenceType协议的基本部分非常简单,只有一个generator()方法,封装了Generator的创建过程。

一旦有了遍历元素的概念,SequenceType立刻就有了非常多的拓展。这里简单列举几个比较关键的:

  • forEach:这个拓展使得我们可以用for循环遍历集合了:for item in sequence

  • dropFirst(n: Int)和dropLast(n: Int):这两个方法返回的是除了前(后)n个元素之外的Sequence。需要提一下的是,由于此时的SequenceType还没有下标的概念,这两个方法的实现是非常复杂的。

  • prefix(maxLength: Int)和suffix(maxLength: Int):和刚刚两个方法类似,这两个方法返回的是前(后)maxLength个元素,实现也不简单。

  • elementsEqual、contains、minelement、maxElement等,这些都是针对元素的判断和选择。

  • map、flatMap、filter、reduce这些方法是针对所有元素的变换。

SequenceType的拓展实在是太多了,但总结来看不外乎两点:

  1. 由于可以多次遍历元素了,我们可以对元素进行各种比较、处理、筛选等等操作。这些派生出来的方法和函数极大的强化了SequenceType的功能。

  2. 由于SequenceType自身的局限性,不能保证一定可以多次遍历所有元素,还没有下标和元素位置的概念,因此某些方法的实现还不够高效,

带着这样的遗憾,我们来看看最关键的CollectionType是如何实现的。

细谈CollectionType

之前我们说过CollectionType协议是在SequenceType的基础上实现了Indexable协议。由于协议的继承关系,任何实现了CollectionType协议的类,必须实现Indexable协议规定的两个参数:startIndex和endindex,以及一个下标脚本:subscript (position: Self.Index) -> Self._Element { get }。即使这三个要求在CollectionType中没有直接标出来。

回顾一下数组定义的前三行,正是满足了这三个要求。再看CollectionType,它不仅重载了Indexable的一个下标脚本,还额外提供了一个下标脚本用来访问某一段元素,这个下标脚本返回的类型是切片(Slice)。这也正是数组定义的第四行,实现的内容。

细心的读者可能已经注意到,CollectionType还定义了很多属性和方法,比如:prefixUpTo、suffixFrom、isEmpty、first等等。但数组没有实现其中的任何一个。

事实上,这不仅不是数组设计的失败之处,而正是Swift协议的强大之处。Swift可以通过协议拓展,为计算属性和方法提供默认实现。因此,数组可以不用写任何代码就具备这些方法。更赞的是,任何实现了CollectionType协议的类型也因此具有了这些方法。

观察一下CollectionType的其它拓展,大部分都是重写了SequenceType中的实现。之前已经提到过SequenceType没有下标的概念,而类似于dropFirst这样的方法,利用下标的概念是非常容易实现的。

除了对一些已有方法的重写之外,CollectionType还新增了一些基于下标的方法。比如indexOf()等。

套用官方文档中对CollectionType的总结就是:

A multi-pass sequence with addressable positions

也就是说CollectionType是可以多次遍历,元素可定位的SequenceType

总结

Element、Generator、SequenceType、CollectionType、Array由下至上构造了数组背后的层次结构。他们的关系如下图所示:

Swift数组层次结构

如果我们希望定义一个自己的数据结构,比如链表。首先可以明确它要实现CollectionType协议。链表应该是和Array同层次的类型。然后我们需要定义一个_ListType的协议,这个协议对应数组的_ArrayList协议,根据数据结构自身的特性定义一些方法。

如果觉得CollectionType甚至是SequenceType不满足我们的需求,我们还可以自己实现对应的类型。难度不会很大,因为它们虽然负责,但大多数方法已有默认实现,我们只要重写一些关键的逻辑即可。

最后需要说明的是,Swift中对集合的实现实在是太复杂了,如果每个都详细分析,怕是可以写一本书。希望读完这篇文章后,读者可以举一反三,自行阅读源码解决相关问题。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


效率成吨提升之代码生成器-蓝湖工具神器iOS,Android,Swift,Flutter
软件简介:蓝湖辅助工具,减少移动端开发中控件属性的复制和粘贴.待开发的功能:1.支持自动生成约束2.开发设置页面3.做一个浏览器插件,支持不需要下载整个工程,可即时操作当前蓝湖浏览页面4.支持Flutter语言模板生成5.支持更多平台,如Sketch等6.支持用户自定义语言模板
【Audio音频开发】音频基础知识及PCM技术详解
现实生活中,我们听到的声音都是时间连续的,我们称为这种信号叫模拟信号。模拟信号需要进行数字化以后才能在计算机中使用。目前我们在计算机上进行音频播放都需要依赖于音频文件。那么音频文件如何生成的呢?音频文件的生成过程是将声音信息采样、量化和编码产生的数字信号的过程,我们人耳所能听到的声音频率范围为(20Hz~20KHz),因此音频文件格式的最大带宽是20KHZ。根据奈奎斯特的理论,音频文件的采样率一般在40~50KHZ之间。奈奎斯特采样定律,又称香农采样定律。...............
见过仙女蹦迪吗?一起用python做个小仙女代码蹦迪视频
前言最近在B站上看到一个漂亮的仙女姐姐跳舞视频,循环看了亿遍又亿遍,久久不能离开!看着小仙紫姐姐的蹦迪视频,除了一键三连还能做什么?突发奇想,能不能把舞蹈视频转成代码舞呢?说干就干,今天就手把手教大家如何把跳舞视频转成代码舞,跟着仙女姐姐一起蹦起来~视频来源:【紫颜】见过仙女蹦迪吗 【千盏】一、核心功能设计总体来说,我们需要分为以下几步完成:从B站上把小姐姐的视频下载下来对视频进行截取GIF,把截取的GIF通过ASCII Animator进行ASCII字符转换把转换的字符gif根据每
自定义ava数据集及训练与测试 完整版 时空动作/行为 视频数据集制作 yolov5, deep sort, VIA MMAction, SlowFast
前言这一篇博客应该是我花时间最多的一次了,从2022年1月底至2022年4月底。我已经将这篇博客的内容写为论文,上传至arxiv:https://arxiv.org/pdf/2204.10160.pdf欢迎大家指出我论文中的问题,特别是语法与用词问题在github上,我也上传了完整的项目:https://github.com/Whiffe/Custom-ava-dataset_Custom-Spatio-Temporally-Action-Video-Dataset关于自定义ava数据集,也是后台
【视频+源码】登录鉴权的三种方式:token、jwt、session实战分享
因为我既对接过session、cookie,也对接过JWT,今年因为工作需要也对接了gtoken的2个版本,对这方面的理解还算深入。尤其是看到官方文档评论区又小伙伴表示看不懂,所以做了这期视频内容出来:视频在这里:本期内容对应B站的开源视频因为涉及的知识点比较多,视频内容比较长。如果你觉得看视频浪费时间,可以直接阅读源码:goframe v2版本集成gtokengoframe v1版本集成gtokengoframe v2版本集成jwtgoframe v2版本session登录官方调用示例文档jwt和sess
【Android App】实战项目之仿微信的私信和群聊App附源码和演示视频 超详细必看
【Android App】实战项目之仿微信的私信和群聊App(附源码和演示视频 超详细必看)
采用MATLAB对正弦信号,语音信号进行生成、采样和恢复,利用MATLAB工具箱对混杂噪声的音频信号进行滤波
采用MATLAB对正弦信号,语音信号进行生成、采样和内插恢复,利用MATLAB工具箱对混杂噪声的音频信号进行滤波
Keras深度学习实战40——音频生成
随着移动互联网、云端存储等技术的快速发展,包含丰富信息的音频数据呈现几何级速率增长。这些海量数据在为人工分析带来困难的同时,也为音频认知、创新学习研究提供了数据基础。在本节中,我们通过构建生成模型来生成音频序列文件,从而进一步加深对序列数据处理问题的了解。
  • • 效率成吨提升之代码生成器-蓝湖工具神器…
  • • 【Audio音频开发】音频基础知识及PCM技…
  • • 见过仙女蹦迪吗?一起用python做个小仙…
  • • 【Android App】实战项目之仿抖音的短视…
  • • 自定义ava数据集及训练与测试 完整版 时…
  • • 【视频+源码】登录鉴权的三种方式:tok…
  • • 【Android App】实战项目之仿微信的私信…
  • • 零基础用Android Studio实现简单的本地…
  • • 采用MATLAB对正弦信号,语音信号进行生…
  • • Keras深度学习实战40——音频生成
  • • 视频实时行为检测——基于yolov5+deeps…
  • • 数电实验 数字电子钟设计 基于quartus …
  • • 腾讯会议使用OBS虚拟摄像头
  • • 文本生成视频Make-A-Video,根据一句话…
  • • 信号处理——MATLAB音频信号加噪、滤波
  • • 【新知实验室 - TRTC 实践】音视频互动…
  • • Keras深度学习实战39——音乐音频分类
  • • C++游戏game | 井字棋游戏坤坤版配资源…

深入探究Swift数组背后的协议、方法、拓展的更多相关文章

  1. html5使用canvas实现弹幕功能示例

    这篇文章主要介绍了html5使用canvas实现弹幕功能示例的相关资料,需要的朋友可以参考下

  2. 前端实现弹幕效果的方法总结(包含css3和canvas的实现方式)

    这篇文章主要介绍了前端实现弹幕效果的方法总结(包含css3和canvas的实现方式)的相关资料,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  3. H5 canvas实现贪吃蛇小游戏

    本篇文章主要介绍了H5 canvas实现贪吃蛇小游戏,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  4. ios – 声明NSDictionary并在Swift中添加键值对?

    我一直在尝试使用类类型键和值来声明一个NSDictionary,如下所示:这里,“Category”和“SubCategory”是全局类.我知道我不能将类类型用于关键字段.但是,无论如何,我应该做到这一点.有没有办法做到这一点?如何声明专门的NSDictionary或类似的东西来做到这一点?

  5. ios – parse.com用于键,预期字符串的无效类型,但是得到了数组

    我尝试将我的数据保存到parse.com.我已经预先在parse.com上创建了一个名为’SomeClass’的类.它有一个名为’mySpecialColumn’的列,其数据类型为String.这是我尝试使用以下代码保存数据的代码:如果我运行这个我得到:错误:密钥mySpecialColumn的无效类型,预期字符串,但得到数组这就是我在parse.com上的核心外观:有谁知道我为什么会收到这个错误?

  6. ios – Swift相当于`[NSDictionary initWithObjects:forKeys:]`

    Swift的原生字典是否与[NSDictionaryinitWithObjects:forKeys:]相当?假设我有两个带键和值的数组,并希望将它们放在字典中.在Objective-C中,我这样做:当然我可以通过两个数组迭代一个计数器,使用vardict:[String:Int]并逐步添加东西.但这似乎不是一个好的解决方案.使用zip和enumerate可能是同时迭代两者的更好方法.然而,这种方法

  7. ios – 上下文类型’NSFastEnumeration’不能与数组文字一起使用

    斯威夫特3,你会这样做吗?解决方法正如您所发现的,您不能使用as-casting将数组文字的类型指定为NSFastEnumeration.您需要找到一个符合NSFastEnumeration的正确类,在您的情况下它是NSArray.通常写这样的东西:

  8. ios – 无法识别的选择器发送到实例NSTimer Swift

    解决方法让updateTime成为一个类方法.如果它是在一个纯粹的Swift类中,你需要在@objc前面说明该方法的声明,如:

  9. ios – 在Swift中获取Cocoa Touch Framework项目版本字符串

    有谁知道这是否是我的项目设置中的缺陷,Xcode中的一个错误,或者是否有一种方法可以将Swift中的框架版本作为String或数组获取,这样我可以提供比major.minor更精细的版本控制?

  10. ios – 搜索数组swift中的对象

    我正在尝试使用UISearchController创建搜索功能.但是,我似乎无法使其与我的团队对象一起工作.我首先创建了一个包含id,name和shortname的TeamObject.然后我从一个url中检索teamData,并将TeamObjects添加到一个填充到tableView中的数组中.这个tableView包含一个searchController,它假设过滤数据,但没有任何反应.阵列

随机推荐

  1. Swift UITextField,UITextView,UISegmentedControl,UISwitch

    下面我们通过一个demo来简单的实现下这些控件的功能.首先,我们拖将这几个控件拖到storyboard,并关联上相应的属性和动作.如图:关联上属性和动作后,看看实现的代码:

  2. swift UISlider,UIStepper

    我们用两个label来显示slider和stepper的值.再用张图片来显示改变stepper值的效果.首先,这三个控件需要全局变量声明如下然后,我们对所有的控件做个简单的布局:最后,当slider的值改变时,我们用一个label来显示值的变化,同样,用另一个label来显示stepper值的变化,并改变图片的大小:实现效果如下:

  3. preferredFontForTextStyle字体设置之更改

    即:

  4. Swift没有异常处理,遇到功能性错误怎么办?

    本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请发送邮件至dio@foxmail.com举报,一经查实,本站将立刻删除。

  5. 字典实战和UIKit初探

    ios中数组和字典的应用Applicationschedule类别子项类别名称优先级数据包contactsentertainment接触UIKit学习用Swift调用CocoaTouchimportUIKitletcolors=[]varbackView=UIView(frame:CGRectMake(0.0,0.0,320.0,CGFloat(colors.count*50)))backView

  6. swift语言IOS8开发战记21 Core Data2

    上一话中我们简单地介绍了一些coredata的基本知识,这一话我们通过编程来实现coredata的使用。还记得我们在coredata中定义的那个Model么,上面这段代码会加载这个Model。定义完方法之后,我们对coredata的准备都已经完成了。最后强调一点,coredata并不是数据库,它只是一个框架,协助我们进行数据库操作,它并不关心我们把数据存到哪里。

  7. swift语言IOS8开发战记22 Core Data3

    上一话我们定义了与coredata有关的变量和方法,做足了准备工作,这一话我们来试试能不能成功。首先打开上一话中生成的Info类,在其中引用头文件的地方添加一个@objc,不然后面会报错,我也不知道为什么。

  8. swift实战小程序1天气预报

    在有一定swift基础的情况下,让我们来做一些小程序练练手,今天来试试做一个简单地天气预报。然后在btnpressed方法中依旧增加loadWeather方法.在loadWeather方法中加上信息的显示语句:运行一下看看效果,如图:虽然显示出来了,但是我们的text是可编辑状态的,在storyboard中勾选Editable,再次运行:大功告成,而且现在每次单击按钮,就会重新请求天气情况,大家也来试试吧。

  9. 【iOS学习01】swift ? and !  的学习

    如果不初始化就会报错。

  10. swift语言IOS8开发战记23 Core Data4

    接着我们需要把我们的Rest类变成一个被coredata管理的类,点开Rest类,作如下修改:关键字@NSManaged的作用是与实体中对应的属性通信,BinaryData对应的类型是NSData,CoreData没有布尔属性,只能用0和1来区分。进行如下操作,输入类名:建立好之后因为我们之前写的代码有些地方并不适用于coredata,所以编译器会报错,现在来一一解决。

返回
顶部