Swift进阶(txt+pdf+epub+mobi电子书下载)


发布时间:2021-02-26 14:54:08

点击下载

作者:Chris Eidhof(克里斯·安道夫)

出版社:电子工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

Swift进阶

Swift进阶试读:

内容简介

本书涵盖了关于Swift程序设计的进阶话题。如果你已经通读Apple的《Swift编程指南》,并且想要深入探索关于这门语言的更多内容,那么这本书正适合你!

Swift非常适合用来进行系统编程,同时它也能被用于书写高层级的代码。我们在书中既会研究像泛型、协议这样的高层级抽象的内容,也会涉足像封装C代码以及字符串内部实现这样的低层级话题。本书将帮助你进一步完善知识体系,带领你从Swift的入门或中级水平迈入Swift高级开发者的大门。

译者序

我经常会收到Swift学习者和使用者发来的电子邮件,问我应该怎么进一步提高自己的Swift水平,而在这种时候,我都会向他们推荐您手中的这本书——《Swift进阶》。

在2017年3月的TIOBE最受欢迎编程语言排行榜中,Swift首次进入前十名,已经将它的“前辈”Objective-C远远抛在脑后;而Swift 3.0的开源及跨平台,也为这门语言的使用范围和持续发展带来了前所未有的机遇和希望。

在Swift高速发展的今天,越来越多的Swift开发者不仅仅满足于完成开发任务,他们更希望能知道如何写出优秀的代码,如何优雅高效地实现功能,以及如何更灵活地使用这门语言来应对改变。想要做到这些,我们就需要做到知其然,并知其所以然。《Swift进阶》正是一本向您介绍Swift的种种语言特性“所以然”的书籍。

(3)的幕后人员一同联合编写。本书原版一经公布,就引起了国外Swift社区的极大关注,可以说是国外高级Swift开发者几乎人手一本的必读读物。书中深入浅出地剖析了Swift里深层次的实现细节以及设计思路。对于包括诸如内建集合类型的底层实现、泛型和协议的设计、Swift字符串的原理和使用逻辑、值类型和引用类型的适用场景和特点等话题,书中都进行了详细的分析。

本书通过这些细致和系统的解释,为我们揭示了Swift的设计哲学,让我们在学习Swift的过程中,从“身在此山”变为“高屋建瓴”。虽然在技术精进的道路上没有捷径,但若将前人的经验和总结的精华作为基础,确实能让我们事半功倍。

技术书籍总会面临版本变动和更新的问题。本书的英文原版是在2015年Swift 2时发布的,其实该书的翻译工作也早在2015年年中就完成了。但是在Swift 3中,Apple对这门语言进行了大幅的重塑和调整,本着对读者负责的态度,我们并没有急于推出本书的过时版本,而是在等待Swift趋于稳定后,直接以对应最新版本的形式进行发布。在能预见的未来中,Swift 4及后续版本并不会发生像前面版本那样的大规模改动,因此我们认为学习和进阶Swift的时机已经成熟。《Swift进阶》一书在探讨问题时也对版本之间的差异进行了说明,让读者可以了解到技术变革的来龙去脉,并为未来的知识更新提前做好准备。

我们必须承认,在国内当前Swift的接受度和使用范围,已经与国外产生了一些差距。由此导致了Swift程序开发的平均水平也稍有落后。但我们相信这只是暂时的,随着Swift社区的日益强大,国内使用Swift的机会和应用场合,都会发生爆发式的增长。让更多的中国开发者有机会接触和了解Swift开发更深层次的内容,正是本书目的所在。王巍第1章 介绍《Swift进阶》对一本书来说是一个很大胆的标题,所以我想我们应该先解释一下它意味着什么。

当我们开始本书第一版的写作时,Swift才刚刚一岁。我们推测这门语言会在进入第二个年头的时候继续高速地发展,不过尽管我们十分犹豫,但还是决定在Swift 2.0测试版发布以前就开始写作。几乎没有别的语言能够在如此短的时间里就能吸引这么多的开发者前来使用。

但是这留给了我们一个问题:如何写出“符合语言习惯”的Swift代码?对于某一个任务,有正确的做法吗?标准库给了我们一些提示,但是我们知道,即使是标准库本身也会随时间发生变化,它常常抛弃一切约定,又去遵守另一些约定。不过,在过去两年里,Swift高速进化着,而优秀的Swift代码标准也日益明确。

对于从其他语言迁移过来的开发者,Swift可能看起来很像你原来使用的语言,特别是它可能拥有你原来使用的语言中你最喜欢的那一部分。它可以像C语言一样进行低层级的位操作,但又可以避免许多未定义行为的陷阱。Ruby的教徒可以在像是map或filter的轻量级的尾随闭包中感受到宾至如归。Swift的泛型和C++的模板如出一辙,但是额外的类型约束能保证泛型方法在被定义时就是正确的,而不必等到使用的时候再进行判定。灵活的高阶函数和运算符重载让你能够以Haskell或者F#那样的风格进行编码。最后@objc关键字允许你像在Objective-C中那样使用selector和各种运行时的动态特性。

有了这些相似点,Swift可以去适应其他语言的风格。比如,Objective-C的项目可以自动地导入Swift中,很多Java或者C#的设计模式也可以直接照搬过来使用。在Swift发布的前几个月,一大波关于单子(monad)的教程和博客也纷至杳来。

但是失望也接踵而至。为什么我们不能像Java中的接口那样将协议扩展(protocol extension)和关联类型(associated type)结合起来使用?为什么数组不具有我们预想那样的协变(covariant)特性?为什么我们无法写出一个“函子”(functor)?有时候这些问题的答案是Swift还没有来得及实现这部分功能,但是更多时候,这是因为在Swift中有其他更适合这门语言的方式来完成这些任务,或者是因为Swift中这些你认为等价的特性其实和你原来的想象大有不同。

译者注:数组的协变特性指的是,包含有子类型对象的数组,可以直接赋值给包含有父类型对象的数组的变量。比如在Java和C#中,string是object的子类型,而对应的数组类型string[]可以直接赋值给声明为object[]类型的变量。但是在Swift中,Array和Array之间并没有这样的关系。

和其他大多数编程语言一样,Swift也是一门复杂的语言。但是它将这些复杂的细节隐藏得很好。你可以使用Swift迅速上手开发应用,而不必知晓泛型、重载或者是静态调用和动态派发之间的区别等这些知识。你可能永远都不会需要去调用C语言的代码,或者实现自定义的集合类型。但是随着时间的推移,无论是想要提升你的代码的性能,还是想让程序更加优雅清晰,抑或是只是为了完成某项开发任务,你都有可能要逐渐接触到这些事情。

带你深入地学习这些特性就是这本书的写作目的。我们在书中尝试回答了很多“这个要怎么做”以及“为什么在Swift中会是这个结果”这样的问题,这种问题遍布各个论坛。我们希望你一旦阅读过本书,就能把握这些语言基础的知识,并且了解很多Swift的进阶特性,从而对Swift是如何工作的有一个更好的理解。本书中的知识点可以说是一个高级Swift程序员所必须了解和熟悉的内容。1.1 本书所面向的读者

本书面向的是有经验的程序员,你不需要是程序开发的专家,不过你应该已经是Apple平台的开发者,或者是想要从其他比如Java或者C++这样的语言转行过来的程序员。如果你想要把你的Swift相关知识技能提升到和你原来已经熟知的Objective-C或者其他语言的同一水平线上,那么这本书会非常适合你。本书也适合那些已经开始学习Swift,对这门语言基础有一定了解,并且渴望再上一个层次的新程序员们。

(2)上均可以下载)。如果你很有把握,你可以尝试同时阅读我们的这本书和Apple的Swift书籍。

这也不是一本教你如何为macOS或者iOS编程的书籍。不可否认,Swift现在主要用于Apple的平台,我们会尽量包含一些实践中使用的例子,但是我们更希望这本书可以对非Apple平台的程序员也有所帮助。1.2 主题

我们按照基本概念的主题来组织本书,其中有一些深入像可选值和字符串这样基本概念的章节,也有对于像C语言互用性方面的主题。不过纵观全书,有一些主题可以描绘出Swift给人的总体印象:

Swift既是一门高层级语言,又是一门低层级语言。你可以在Swift中用map或者reduce来写出十分类似于Ruby和Python的代码,你也可以很容易地创建自己的高阶函数。Swift让你有能力快速完成代码编写,并将它们直接编译为原生的二进制可执行文件,这使得其在性能上可以与用C语言编写的程序相媲美。

兼顾高低两个层级。将一个数组通过闭包表达式映射到另一个数组所编译得到的汇编码,与直接对一块连续内存进行循环所得到的结果是一致的。

不过,为了最大化地利用这些特性,有一些知识你需要掌握。如果你能对结构体和类的区别有深刻理解,或者对动态和静态方法派发的不同了然于胸,那么你就能从中获益。我们会在之后更深入地介绍这些内容。

Swift是一门多范式的语言。你可以用Swift来编写面向对象的代码,也可以使用不变量的值来写纯函数式的程序,在必要的时候,你甚至还能使用指针运算来写和C类似的代码。

这是一把双刃剑。好的一面,在Swift中你将有很多可用的工具,你也不会被限制在一种代码写法里。但是这也让你身临险境,因为实际上你可能会变成使用Swift语言来书写Java或者C或者Objective-C的代码。

Swift仍然可以使用大部分Objective-C的功能,包括消息发送,运行时的类型判定,以及KVO等。但是Swift还引入了很多Objective-C中不具备的特性。

(3)说道:

现在,相比Haskell,Swift可能是更好,更有价值,也更合适用来的学习函数式编程的语言。

Swift拥有泛型、协议、值类型以及闭包等特性,这些特性是对函数式风格的很好的介绍。我们甚至可以将运算符和函数结合起来使用。在Swift早期,这门语言为世界带来了很多关于单子(monad)的博客。不过等到Swift 2.0发布并引入协议扩展的时候,大家研究的趋势也随之发生了变化。

(4)这本书的介绍中,Paul Graham写道:

富有经验的Lisp程序员将他们的程序拆分成不同的部分。除了自上而下的设计原则,他们还遵循一种可以被称为自下而上的设计,他们可以将语言进行改造,让它更适合解决当前的问题。在Lisp中,你并不只是使用这门语言来编写程序,在开发过程中,你同时也在构建这门语言。当你编写代码的时候,你可能会想“要是Lisp有这个或者这个运算符就好了”,之后你就真的可以去实现一个这样的运算符。事后来看,你会意识到使用新的运算符可以简化程序的某些部分的设计,语言和程序就这样相互影响,发展进化。

Swift的出现比Lisp要晚得多,不过,我们能强烈感受到Swift也鼓励从下向上的编程方式。这让我们能轻而易举地编写一些通用可重用组件,然后可以将它们组合起来实现更强大的的特性,最后用它们来解决实际问题。Swift非常适合用来构建这些组件,你可以使它们看起来就像是语言自身的一部分。一个很好的例子就是Swift的标准库,许多你能想到的基本组件——像可选值和基本的运算符等——其实都不是直接在语言本身中定义的,相反,它们是在标准库中被实现的。

Swift代码可以做到紧凑、精确,同时保持清晰。Swift使用相对简洁的代码,但这并不意味着其单纯地减少代码的输入量,还标志了一个更深层次的目标。Swift的观点是,通过抛弃你经常在其他语言中见到的模板代码,而使得代码更容易被理解和阅读。这些模板代码往往会成为理解程序的障碍,而非助力。

举个例子,有了类型推断,在上下文很明显的时候我们就不再需要乱七八糟的类型声明了;那些几乎没有意义的分号和括号也都被移除了;泛型和协议扩展让你免于重复,并且把通用的操作封装到可以复用的方法中。这些特性最终的目的都是为了能够让代码看上去一目了然。

一开始,这可能会对你造成一些困扰。如果你以前从来没有用像是map、filter和reduce这样的函数,那么它们可能看起来比简单的for循环要难理解。但是我们相信这个学习过程会很短,并且作为回报,你会发现这样的代码你第一眼看上去就能更准确地判断出它“显然正确”。

除非你有意为之,否则Swift在实践中总是安全的。Swift和C或者C++这样的语言不同,在那些语言中,你只要忘了做某件事情,你的代码很可能就不是安全的了。它和Haskell或者Java也不一样,在后两者中有时候不论你是否需要,它们都“过于”安全。

(5)一些经验教训:

有时候你需要为那些构建架构的专家实现一些特性,这些特性应当被清晰地标记为危险——它们往往并不能很好地对应其他语言中某些有用的特性。

说这段话时,Eric特别所指的是C#中的终止方法(finalizer),它和C++中的析构函数(destructor)比较类似。但是不同于析构函数,终止方法的运行是不确定的,它受命于垃圾回收器,并且运行在垃圾回收的线程上。更糟糕的是,很可能终止方法甚至完全不会被调用。但是,在Swift中,因为采用的是引用计数,deinit方法的调用是可以确定和预测的。

Swift的这个特点在其他方面也有体现。未定义的和不安全的行为默认是被屏蔽的。比如,一个变量在被初始化之前是不能使用的,使用越界下标访问数组将会抛出异常,而不是继续使用一个可能取到的错误值。

当你真正需要的时候,也有不少“不安全”的方式,比如unsafeBitcast函数,或者是UnsafeMutablePointer类型。但是强大能力的背后是更大的未定义行为的风险。比如下面的代码:var someArray=[1,2,3]let uhOh=someArray.withUnsafeBufferPointer{ptr in //ptr只在这个block中有效 //不过你完全可以将它返回给外部世界: return ptr}//稍后...print(uhOh[10])

这段代码可以编译,但是天知道它最后会做什么。方法名里已经警告了你这是不安全的,所以对此你需要自己负责。

Swift是一门独断的语言。关于“正确的”Swift编码方法,作为本书作者,我们有着坚定的自己的看法。你会在本书中看到很多这方面的内容,有时候我们会把这些看法作为事实来对待。但是,归根结底,这只是我们的看法,你完全可以反对我们的观点。Swift还是一门年轻的语言,许多事情还未成定局。更糟糕的是,很多博客或者文章是不正确的,或者已经过时(包括我们曾经写过的一些内容,特别是早期就完成了的内容)。不论你在读什么资料,最重要的事情是你应当亲自尝试,去检验它们的行为,并且去体会这些用法。带着批判的眼光去审视和思考,并且警惕那些已经过时的信息。1.3 术语

你用,或是不用,术语就在那里,不多不少。你懂,或是不懂,定义就在那里,不偏不倚。

(6)。为了避免困扰,接下来我们会介绍一些贯穿于本书的术语定义。我们会尽可能遵守Swift官方文档中的术语用法,使用被Swift社区所广泛接受的定义。这些定义大多都会在接下来的章节中被详细介绍,所以就算一开始你对它们一头雾水,也大可不必在意。如果你已经对这些术语非常了解,那么我们也还是建议你再浏览一下它们,并且确定你能接受我们的表述。

在Swift中,我们需要对值、变量、引用以及常量加以区分。

字面量(literal)的例子,值也可以是运行代码时生成的。当你计算5的平方时,你得到的数字也是一个值。

改变(mutating)。

常量变量(constant variables),或者简称为常量。一旦常量被赋予一个值,它就不能再次被赋一个新的值了。

我们不需要在一个变量被声明的时候就立即为它赋值。我们可以先对变量进行声明(let x:Int),然后稍后再给它赋值(x=1)。Swift是强调安全的语言,它将检查所有可能的代码路径,并确保变量在被读取之前一定是完成了赋值的。在Swift中变量不会存在未定义状态。当然,如果一个变量是用let声明的,那么它只能被赋值一次。

值类型(value type)。当你把一个结构体变量赋值给另一个变量,那么这两个变量将会包含同样的值。你可以将它理解为内容被复制了一遍,但是更精确地描述,则是被赋值的变量与另外的那个变量包含了同样的值。

引用(reference)是一种特殊类型的值:它是一个“指向”另一个值的值。两个引用可能会指向同一个值,这引入了一种可能性,那就是这个值可能会被程序的两个不同的部分所改变。

对象(object),这个术语经常被滥用,会让人困惑)。对于一个类的实例,我们只能在变量里持有对它的引用,然后使用这个引用来访问它。

同一性(identity),也就是说,你可以使用===来检查两个变量是否确实引用了同一个对象。如果相应类型的==运算符被实现了,那你也可以用==来判断两个变量是否相等。两个不同的对象按照定义也是可能相等的。

引用相等。

在Swift中,类引用不是唯一的引用类型。Swift中依然有指针,比如使用withUnsafeMutable-Pointer和类似方法所得到的就是指针。不过类是使用起来最简单引用类型,这与它们的引用特性被部分隐藏在语法糖之后是不无关系的。你不需要像在其他一些语言中那样显式地处理指针的“解引用”。(在互用性章节中会详细提及其他种类的引用。)

知道这个变量持有的是值类型还是引用类型。

值语义(value semantics)。这种复制可能是在赋值新变量时就发生的,也可能会延迟到变量内容发生变更的时候。

浅复制(shallow copy)。

举个例子,Foundation框架中的Data结构体实际上是对引用类型NSData的一个封装。不过, Data的作者采取了额外的步骤,来保证当Data结构体发生变化的时候对其中的NSData对象进行深复制。他们使用一种名为“写时复制”(copy-on-write)的技术来保证操作的高效,我们会在结构体和类章节里详细介绍这种机制。现在我们需要重点知道的是,这种写时复制的特性并不是直接具有的,它需要额外进行实现。

在Swift中,像是数组这样的集合类型也都是对引用类型的封装,它们同样使用了写时复制的方式来在提供值语义的同时保持高效。不过,如果集合类型的元素是引用类型(比如一个含有对象的数组) ,那么对象本身将不会被复制,只有对它的引用会被复制。也就是说, Swift的数组只有当其中的元素满足值语义时,数组本身才具有值语义。在下一章,我们会讨论Swift中的集合类型与Foundation框架中NSArray和NSDictionary这些集合类型的不同之处。

有些类是完全不可变的,也就是说,从被创建以后,它们就不提供任何方法来改变它们的内部状态。这意味着即使它们是类,它们依然具有值语义(因为它们就算被到处使用也从不会改变)。但是要注意的是,只有那些标记为final的类能够保证不被子类化,也不会被添加可变状态。

高阶函数(higher-order function)。

闭包(closure)。

闭包表达式(closure expression)来定义。有时候我们只把通过闭包表达式创建的函数叫作“闭包”,不过不要让这种叫法蒙蔽了你的双眼。实际上使用func关键字定义的函数也是闭包。

函数是引用类型。也就是说,将一个通过闭合变量保存状态的函数赋值给另一个变量,并不会导致这些状态被复制。和对象引用类似,这些状态会被共享。换句话说,当两个闭包持有同样的局部变量时,它们是共享这个变量以及它的状态的。这可能会让你有点儿惊讶,我们将在函数一章中涉及这方面的更多内容。

自由函数(free function),这可以将它们与方法区分开来。

内联(inline)这些函数,也就是说,完全不去做函数调用,而是将这部分代码替换为需要执行的函数。静态派发还能够帮助编译器丢弃或者简化那些在编译时就能确定不会被实际执行的代码。

(7)来完成,要么通过selector和objc_msgSend来完成,前者的处理方式和Java或是C++中类似,而后者只针对@objc的类和协议上的方法。

重载(overloading),它是指为不同的类型多次写同一个函数的行为。(注意不要把重写和重载弄混了,它们是完全不同的。)实现多态的第三种方法是通过泛型,也就是一次性地编写能够接受任意类型的函数或者方法,不过这些方法的实现会各有不同。与方法重写不同的是,泛型中的方法在编译期间就是静态已知的。我们会在泛型章节中提及关于这方面的更多内容。1.4 Swift风格指南

当我们编写这本书,或者在我们的项目中使用Swift代码时,我们尽量遵循如下的原则:● 对于命名,在使用时能清晰表意是最重要的。因为API被使用的

次数要远远多于被声明的次数,所以我们应当从使用者的角度来(8)

考虑它们的名字。尽快熟悉Swift API设计准则,并且在自己的

代码中坚持使用这些准则。● 简洁经常有助于代码清晰,但是简洁本身不应该成为我们编码的

目标。● 务必为函数添加文档注释——特别是泛型函数。● 类型使用大写字母开头,函数、变量和枚举成员使用小写字母开

头,两者都使用驼峰式命名法。● 使用类型推断。省略显而易见的类型会有助于提高代码的可读性。● 如果存在歧义或者在进行定义的时候不要使用类型推断(比如

func就需要显式地指定返回类型)。● 优先选择结构体,只在确实需要使用到类特有的特性或者是引用

语义时才使用类。● 除非你的设计就是希望某个类被继承使用,否则都应该将它们标

记为final。● 除非一个闭包后面立即跟随有左括号,否则都应该使用尾随闭包

(trailing closure)的语法。● 使用guard来提早退出的方法。● 避免对可选值进行强制解包和隐式强制解包。它们偶尔有用,但

是如果经常需要使用它们,则往往意味着有其他不妥的地方。● 不要写重复的代码。如果你发现你写了好几次类似的代码片段,

那么试着将它们提取到一个函数里,并且考虑将这个函数转化为

协议扩展的可能性。● 试着去使用map和reduce,但这不是强制的。在合适的时候,使

用for循环也无可厚非。高阶函数的意义是让代码的可读性更

高。但是如果使用reduce的场景难以理解,则强行使用往往事与

愿违,这种时候使用简单的for循环可能会更清晰。● 试着去使用不可变值:除非你需要改变某个值,否则都应该使用

let来声明变量。不过如果能让代码更加清晰高效的话,也可以选

择使用可变的版本。用函数将可变的部分封装起来,可以把它带

来的副作用进行隔离。● Swift的泛型可能会导致非常长的函数签名。坏消息是我们现在

除了将函数声明强制写成几行,对此并没有什么好办法。我们会

在示例代码中在这点上保持一贯性,这样你能看到我们是如何处

理这个问题的。● 除非你确实需要,否则不要使用self.。在闭包表达式中,使用

self是一个清晰的信号,表明闭包将会捕获self。● 尽可能地对现有的类型和协议进行扩展,而不是写一些全局函

数。这有助于提高可读性,让别人更容易发现你的代码。

————————————————————

(1) https://itunes.apple.com/us/book/swift-programming-language/id1002622538

(2) https://developer.apple.com/swift/resources/

(3) https://twitter.com/headinthebox/status/655407294969196544

(4) http://www.paulgraham.com/onlisp.html

(5) http://www.informit.com/articles/article.aspx?p=2425867

(6) https://zh.wikipedia.org/wiki/行話

(7) https://en.wikipedia.org/wiki/Virtual_method_table

(8) https://swift.org/documentation/api-design-guidelines/第2章 内建集合类型

在所有的编程语言中,元素的集合都是最重要的数据类型。在语言层面上,对于不同类型的容器的良好支持,是决定编程效率和幸福指数的重要因素。Swift在序列和集合这方面进行了特别的强调,标准库的开发者对于该话题的内容所投入的精力远超其他部分。正是有了这样的努力,我们才能够使用到非常强大的集合模型,它比你所习惯的其他语言的集合拥有更好的可扩展性,不过同时它也相当复杂。

在本章中,我们会讨论Swift中内建的几种主要集合类型,并重点研究如何以符合语言习惯的方式高效地使用它们。在下一章中,我们会沿着抽象的阶梯蜿蜒而上,去探究标准库中的集合协议的工作原理。2.1 数组数组和可变性

在Swift中最常用的集合类型非数组莫属。数组是一系列相同类型的元素的有序的容器,对于其中每个元素,我们可以使用下标对其直接进行访问(这又被称作随机访问)。举个例子,要创建一个数字的数组,我们可以这么写://斐波那契数列let fibs=[0, 1, 1, 2, 3, 5]

要是我们使用像是append(_:)这样的方法来修改上面定义的数组,则会得到一个编译错误。这是因为在上面的代码中数组是用let声明为常量的。在很多情景下,这是正确的做法,它可以避免我们不小心对数组做出改变。如果我们想按照变量的方式来使用数组,则需要将它用var来进行定义:var mutableFibs=[0, 1, 1, 2, 3, 5]

现在我们就能很容易地为数组添加单个或是一系列元素了:mutableFibs.append(8)mutableFibs.append(contentsOf:[13, 21])mutableFibs//[0, 1, 1, 2, 3, 5, 8, 13, 21]

可以改变的。我们将在结构体和类中更加详尽地介绍两者的区别。数组和标准库中的所有集合类型一样,是具有值语义的。当你创建一个新的数组变量并且把一个已经存在的数组赋值给它的时候,这个数组的内容会被复制。举个例子,在下面的代码中,x将不会被更改:var x=[1,2,3]var y=xy.append(4)y//[1, 2, 3, 4]x//[1, 2, 3]

var y=x语句复制了x,所以在将4添加到y末尾的时候,x并不会发生改变,它的值依然是[1,2,3]。当你把一个数组传递给一个函数时,会发生同样的事情;方法将得到这个数组的一份本地副本,所有对它的改变都不会影响调用者所持有的数组。

不能保证这个数组不会被改变:let a=NSMutableArray(array:[1,2,3])//我们不想让b发生改变let b:NSArray=a//但是事实上它依然能够被a影响并改变a.insert(4, at:3)b//( 1, 2, 3, 4 )

正确的方式是在赋值时,先手动进行复制:let c=NSMutableArray(array:[1,2,3])//我们不想让d发生改变let d=c.copy() as! NSArrayc.insert(4, at:3)d//( 1, 2, 3 )

在上面的例子中,显而易见,我们需要进行复制,因为a的声明毕竟是可变的。但是,当把数组在方法和函数之间来回传递的时候,事情可能就不那么明显了。

而在Swift中,相比于NSArray和NSMutableArray两种类型,数组只有一种统一的类型,那就是Array。使用var可以将数组定义为可变的,但是区别于NS的数组,当你使用let定义第二个数组,并将第一个数组赋值给它,也可以保证这个新的数组是不会改变的,因为这里没有共用的引用。

创建如此多的复制有可能造成性能问题,不过实际上,Swift标准库中的所有集合类型都使用了“写时复制”这一技术,它能够保证只在必要的时候对数据进行复制。在我们的例子中,直到y.append被调用的之前,x和y都将共享内部的存储。在结构体和类中我们也将仔细研究值语义,并告诉你如何为自己的类型实现写时复制特性。数组和可选值

Swift数组提供了你能想到的所有常规操作方法,像isEmpty或是count。数组也允许直接使用特定的下标直接访问其中的元素,像fibs[3]。不过要牢记,在使用下标获取元素之前,你需要确保索引值没有超出范围。比如获取索引值为3的元素,你需要保证数组中至少有4个元素。否则,你的程序将会崩溃。

这么设计的主要原因是我们可以使用数组切片。在Swift中,计算一个索引值这种操作是非常罕见的:● 想要迭代数组?for x in array● 想要迭代除第一个元素以外的数组其余部分?for x in

array.dropFirst()● 想要迭代除最后5个元素以外的数组?for x in array.dropLast(5)● 想要列举数组中的元素和对应的下标?for (num, element) in

collection.enumerated()● 想要寻找一个指定元素的位置?if let

idx=array.index{someMatchingLogic($0)}● 想要对数组中的所有元素进行变形?

array.map{someTransformation($0)}● 想要筛选出符合某个标准的元素?array.filter{someCriteria($0)}

Swift 3中传统的C风格的for循环被移除了,这是Swift不鼓励你去做索引计算的另一个标志。手动计算和使用索引值往往可能带来很多潜在的bug,所以最好避免这么做。如果这不可避免,那么我们可以很容易地写一个可重用的通用函数来进行处理,在其中你可以对精心测试后的索引计算进行封装,你会在泛型一章里看到这个例子。

知道这些下标都是有效的。这一方面十分麻烦,另一方面也是一个坏习惯。当强制解包变成一种习惯后,很可能你会不小心强制解包了本来不应该解包的东西。所以,为了避免这个行为变成习惯,数组根本没有给出可选值的选项。

无效的下标操作会造成可控的崩溃,有时候这种行为可能会被叫作不安全,但是这只是安全性的一个方面。下标操作在内存安全的意义上是完全安全的,标准库中的集合总是会执行边界检查,并禁止那些越界索引对内存的访问。

其他操作的行为略有不同。first和last属性返回的是可选值,当数组为空时,它们返回nil。first相当于isEmpty ? nil:self[0]。类似地,如果数组为空时,则removeLast将会导致崩溃,而popLast将在数组不为空时删除最后一个元素并返回它,在数组为空时,它将不执行任何操作,直接返回nil。你应该根据自己的需要来选取到底使用哪一个:当你将数组当作栈来使用时,你可能总是想要将empty检查和移除最后元素组合起来使用;而另一方面,如果你已经知道数组一定非空,那么再去处理可选值就完全没有必要了。

我们会在本章后面讨论字典的时候再次遇到关于这部分的权衡。除此之外,关于[可选值]本书会有一整章的内容对它进行讨论。数组变形

Map

对数组中的每个值执行转换操作是一个很常见的任务。每个程序员可能都写过上百次这样的代码:创建一个新数组,对已有数组中的元素进行循环依次取出其中的元素,对取出的元素进行操作,并把操作的结果加入到新数组的末尾。比如,下面的代码计算了一个整数数组里的元素的平方:var squared:[Int]=[]for fib in fibs{ squared.append(fib*fib)}squared//[0, 1, 1, 4, 9, 25]

Swift数组拥有map方法,这个方法来自函数式编程的世界。下面的例子使用了map来完成同样的操作:let squares=fibs.map{fib in fib*fib}squares//[0, 1, 1, 4, 9, 25]

这种版本有三大优势。首先,它很短。长度短一般意味着错误少,不过更重要的是,它比原来更清晰。所有无关的内容都被移除了,一旦你习惯了map满天飞的世界,你就会发现map就像是一个信号,一旦你看到它,就会知道即将有一个函数被作用在数组的每个元素上,并返回另一个数组,它将包含所有被转换后的结果。

其次,squared将由map的结果得到,我们不会再改变它的值,所以也就不再需要用var来进行声明了,我们可以将其声明为let。另外,由于数组元素的类型可以从传递给map的函数中推断出来,我们也不再需要为squared显式地指明类型了。

最后,创造map函数并不难,你只需要把for循环中的代码模板部分用一个泛型函数封装起来就可以了。下面是一种可能的实现方式(在Swift中,它实际上是Sequence的一个扩展,我们会在之后关于编写泛型算法的章节里继续Sequence的话题):extension Array{ func map(_transform:(Element)->T)->[T]{ var result:[T]=[] result.reserveCapacity(count) for x in self{ result.append(transform(x)) } return result }}

Element是数组中包含的元素类型的占位符,T是元素转换之后的类型的占位符。map函数本身并不关心Element和T究竟是什么,它们可以是任意类型。T的具体类型将由调用者传入给map的transform方法的返回值类型来决定。

实际上,这个函数的签名应该是:

func map(_transform:(Element) throws->T) rethrows->[T]

也就是说,对于可能抛出错误的变形函数,map将会把错误转发给调用者。我们会在错误处理一章里覆盖这个细节。在这里,我们选择去掉错误处理的这个修饰,这样看起来会更简单一些。如果你感兴趣,那么可以看看GitHub上Swift仓库的Sequence.map的源码实现[1]。

[1] https://github.com/apple/swift/blob/master/stdlib/public/core/Sequence.swift

使用函数将行为参数化

即使你已经很熟悉map了,也请花一点时间来想一想map的代码。是什么让它可以如此通用而且有用?

map可以将模板代码分离出来,这些模板代码并不会随着每次调用发生变动,发生变动的是那些功能代码,也就是如何变换每个元素的逻辑代码。map函数通过接受调用者所提供的变换函数作为参数来做到这一点。

纵观标准库,我们可以发现很多这样将行为进行参数化的设计模式。标准库中有不下十多个函数接受调用者传入的闭包,并将它作为函数执行的关键步骤:● map和flatMap——如何对元素进行变换。● filter——元素是否应该被包含在结果中。● reduce——如何将元素合并到一个总和的值中。● sequence——序列中下一个元素应该是什么。● forEach——对于一个元素,应该执行怎样的操作。● sort,lexicographicCompare和partition——两个元素应该以怎样

的顺序进行排列。● index,first和contains——元素是否符合某个条件。● min和max——两个元素中的最小值/最大值是哪个。● elementsEqual和starts——两个元素是否相等。● split——这个元素是否是一个分割符。

所有这些函数的目的都是为了摆脱代码中那些杂乱无用的部分,比如像创建新数组,对源数据进行for循环之类的事情。这些杂乱代码都被一个单独的单词替代了。这可以重点突出那些程序员想要表达的真正重要的逻辑代码。

这些函数中有一些拥有默认行为。除非你进行过指定,否则sort默认会把可以做比较的元素按照升序排列。contains对于可以判等的元素,会直接检查两个元素是否相等。这些行为让代码变得更加易读。升序排列非常自然,因此array.sort()的意义也很符合直觉。而对于array.index(of:"foo")这样的表达方式,也要比array.index{$0=="foo"}更容易理解。

不过在上面的例子中,它们都只是特殊情况下的简写。集合中的元素并不一定需要可以做比较,也不一定需要可以判等。你可以不对整个元素进行操作,比如,对一个包含人的数组,你可以通过他们的年龄进行排序(people.sort{$0.age < $1.age}),或者检查集合中有没有包含未成年人(people.contains{$0.age < 18})。你也可以对转变后的元素进行比较,比如通过people.sort{$0.name.uppercased()<$1.name.uppercased()}来进行忽略大小写的排序,虽然这么做的效率不会很高。

还有一些其他类似的很有用的函数,可以接受一个闭包来指定行为。虽然它们并不存在于标准库中,但是你可以很容易地定义和实现它们,我们也建议你尝试着做做看:● accumulate——累加,和reduce类似,不过是将所有元素合并到

一个数组中,而且保留合并时每一步的值。● all(matching:)和none(matching:)——测试序列中是不是所有元素

都满足某个标准,以及是不是没有任何元素满足某个标准。它们

可以通过contains和它进行了精心对应的否定形式来构建。● count(where:)——计算满足条件的元素的个数,和filter相似,但

是不会构建数组。● indices(where:)——返回一个包含满足某个标准的所有元素的索

引的列表,和index( where:)类似,但是不会在遇到首个元素时就

停止。● prefix(while:)——当判断为真的时候,将元素滤出到结果中。一

旦不为真,就将剩余的抛弃。和filter类似,但是会提前退出。这

个函数在处理无限序列或者是延迟计算(lazily-computed)的序列

时会非常有用。● drop(while:)——当判断为真的时候,则丢弃元素。一旦不为

真,则返回其余的元素。和prefix(while:)类似,不过返回相反的

集合。

(1),它们没有来得及被添加到Swift 3.0中,不过会在未来的版本中被添加。)

有时候你可能会发现你写了好多次同样模式的代码,比如想要在一个逆序数组中寻找第一个满足特定条件的元素:let names=["Paula","Elena","Zoe"]var lastNameEndingInA:String?for name in names.reversed() where name.hasSuffix("a"){ lastNameEndingInA=name break}lastNameEndingInA//Optional("Elena")

在这种情况下,你可以考虑为Sequence添加一个小扩展,将这个逻辑封装到last(where:)方法中。我们使用闭包来对for循环中发生的变化进行抽象描述:extension Sequence{ func last(where predicate:(Iterator.Element)->Bool)->Iterator.Element?{ for element in reversed() where predicate(element){ return element } return nil }}

现在我们就能把代码中的for循环换成findElement了:let match=names.last{$0.hasSuffix("a")}match//Optional("Elena")

这么做的好处和我们在介绍map时所描述的是一样的,相比for循环,last(where:)的版本显然更加易读。虽然for循环也很简单,但是在你的头脑里你始终还是要去做一个循环,这加重了理解的负担。使用last(where:)可以减少出错的可能性,而且它允许你使用let而不是var来声明结果变量。

它和guard一起也能很好地工作,可能你会想要在元素没被找到的情况下提早结束代码:guard let match=someSequence.last(where:{$0.passesTest()}) else{return}//对match进行操作

我们在本书后面会进一步涉及扩展集合类型和使用函数的相关内容。

可变和带有状态的闭包

当遍历一个数组的时候,你可以使用map来执行一些其他操作(比如将元素插入到一个查找表中)。我们不推荐这么做,来看看下面这个例子:array.map{item in table.insert(item)}

这将副作用(改变了查找表)隐藏在了一个看起来只是对数组变形的操作中。在上面这样的例子中,使用简单的for循环显然是比使用map这样的函数更好的选择。有一个叫作forEach的函数,看起来很符合我们的需求,但是forEach本身存在一些问题,后面会详细讨论。

局部状态有本质的不同。闭包是指那些可以捕获自身作用域之外的变量的函数,闭包再结合上高阶函数,将成为强大的工具。举个例子,前面我们提到的accumulate函数就可以用map结合一个带有状态的闭包来进行实现:extension Array{ func accumulate(_initialResult:Result, _nextPartialResult:(Result, Element)->Result)->[Result] { var running=initialResult return map{next in running=nextPartialResult(running, next) return running } }}

这个函数创建了一个中间变量来存储每一步的值,然后使用map来从这个中间值逐步创建结果数组:[1,2,3,4].accumulate(0,+)//[1, 3, 6, 10]

要注意的是,这段代码假设了变形函数是以序列原有的顺序执行的。在我们上面的map中,事实确实如此。但是也有可能对于序列的变形是无序的,比如我们可以有并行处理元素变形的实现。官方标准库中的map版本没有指定它是否会按顺序来处理序列,不过看起来现在这么做是安全的。

Filter

另一个常见操作是检查一个数组,然后将这个数组中符合一定条件的元素过滤出来并用它们创建一个新的数组。对数组进行循环并且根据条件过滤其中元素的模式可以用数组的filter方法表示:nums.filter{num in num%2==0}//[2, 4, 6, 8, 10]

我们可以使用Swift内建的用来代表参数的简写$0,这样代码将会更加简短。我们可以不用写出num参数,而将上面的代码重写为:nums.filter{$0%2==0}//[2, 4, 6, 8, 10]

对于很短的闭包,这样做有助于提高可读性。但是如果闭包比较复杂,更好的做法应该是像我们之前那样,显式地把参数名字写出来。不过这更多的是一种个人选择,使用一眼看上去更易读的版本就好。一个不错的原则是,如果闭包可以很好地写在一行里,那么使用简写名会更合适。

通过组合使用map和filter,我们现在可以轻易完成很多数组操作,而不需要引入中间数组。这会使得最终的代码变得更短更易读。比如,寻找100以内同时满足是偶数并且是其他数字的平方的数,我们可以对0..<10进行map来得到所有平方数,然后再用filter过滤出其中的偶数:(1..<10).map{$0*$0}.filter{$0%2==0}//[4, 16, 36, 64]filter的实现看起来和map很类似:extension Array{ func filter(_isIncluded:(Element)->Bool)->[Element]{ var result:[Element]=[] for x in self where isIncluded(x){ result.append(x) } return result }}

如果你对在for中所使用的where感兴趣,则可以阅读可选值一章。

一个关于性能的小提示:如果你正在写下面这样的代码,请不要这么做!bigArray.filter { someCondition}.count>0

filter会创建一个全新的数组,并且会对数组中的每个元素都进行操作。然而在上面这段代码中,这显然是不必要的。上面的代码仅仅检查了是否有至少一个元素满足条件,在这个情景下,使用contains(where:)更为合适:bigArray.contains { someCondition}

这种做法会比原来快得多,主要因为两个方面:它不会为了计数而创建一整个全新的数组,并且一旦匹配了第一个元素,它就将提前退出。一般来说,你只应该在需要所有结果时才去选择使用filter。

有时候你会发现你想用contains完成一些操作,但是写出来的代码看起来很糟糕。比如,要是你想检查一个序列中的所有元素是否全部都满足某个条件,则可以用!sequence.contains{ !condition },其实你可以用一个更具有描述性名字的新函数将它封装起来:extension Sequence{ public func all(matching predicate:(Iterator.Element)->Bool)->Bool{ //对于一个条件,如果没有元素不满足它,那么意味着所有元素都满足它: return !contains{!predicate($0)} }}let evenNums=nums.filter{ $0%2==0 }//[2, 4, 6, 8, 10]evenNums.all{ $0%2==0 }//true

Reduce

map和filter都作用在一个数组上,并产生另一个新的、经过修改的数组。不过有时候,你可能会想把所有元素合并为一个新的值。比如,要是我们想将元素的值全部加起来,可以这样写:var total=0for num in fibs{ total=total+num}total//12

reduce方法对应这种模式,它把一个初始值(在这里是0)以及一个将中间值(total)与序列中的元素(num)进行合并的函数进行了抽象。使用reduce,我们可以将上面的例子重写为这样:let sum=fibs.reduce(0){total, num in total+num}//12

运算符也是函数,所以我们也可以把上面的例子写成这样:fibs.reduce(0,+)//12

reduce的输出值的类型可以和输入的类型不同。举个例子,我们可以将一个整数的列表转换为一个字符串,这个字符串中每个数字后面跟一个空格:fibs.reduce(""){str, num in str+"\(num)"}//0 1 1 2 3 5

reduce的实现是这样的:extension Array{ func reduce(_initialResult:Result, _nextPartialResult:(Result, Element)->Result)->Result { var result=initialResult for x in self{ result=nextPartialResult(result, x) } return result }}

另一个关于性能的小提示:reduce相当灵活,所以在构建数组或者是执行其他操作时看到reduce的话也不足为奇,比如,你可以只使用reduce就能实现map和filter:extension Array{ func map2(_transform:(Element)->T)->[T]{ return reduce([]){ $0+[transform($1)] } } func filter2(_isIncluded:(Element)->Bool)->[Element]{ return reduce([]){ isIncluded($1) ?$0+[$1]:$0 } }}

2),而不是O(n)。随着数组长度的增加,执行这些函数所消耗的时间将以平方关系增加。

flatMap

有时候我们会想要对一个数组用一个函数进行map,但是这个变形函数返回的是另一个数组,而不是单独的元素。

举个例子,假如我们有一个叫作extractLinks的函数,它会读取一个Markdown文件,并返回一个包含该文件中所有链接的URL的数组。这个函数的类型是这样的:func extractLinks(markdownFile:String)->[URL]

如果我们有一系列的Markdown文件,并且想将这些文件中所有的链接都提取到一个单独的数组中,那么我们可以尝试使用markdownFiles.map(extractLinks)来构建。不过问题是这个方法返回的是一个包含了URL的数组的数组,这个数组中的每个元素都是一个文件中的URL的数组。为了得到一个包含所有URL的数组,你还要对

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载