[TOC]
Swift基础(第一天)
Swift 简介
Swift 既是一⻔高层级语言,又是一⻔低层级语言
你可以在 Swift 中用 map 或者 reduce 来 写出十分类似于 Ruby 和 Python 的代码,你也可以很容易地创建自己的高阶函数。Swift 让你 有能力快速完成代码编写,并将它们直接编译为原生的二进制可执行文件,这使得性能上可以 与 C 代码编写的程序相媲美。Swift 真正激动人心,以及令人赞叹的是,我们可以兼顾高低两个层级。将一个数组通过闭包表 达式映射到另一个数组所编译得到的汇编码,与直接对一块连续内存进行循环所得到的结果是 一致的。
Swift 是一⻔多范式的语言
可以用 Swift 来编写面向对象的代码,也可以使用不变量的值来 写纯函数式的程序,在必要的时候,你甚至还能使用指针运算来写和 C 类似的代码。
这是一把双刃剑。好的一面,在 Swift 中你将有很多可用工具,你也不会被限制在一种代码写 法里。但是这也让你身临险境,因为可能你实际上会变成使用 Swift 语言来书写 Java 或者 C 或 者 Objective-C 的代码。
Swift 仍然可以使用大部分 Objective-C 的功能,包括消息发送,运行时的类型判定,以及 KVO 等。但是 Swift 还引入了很多 Objective-C 中不具备的特性。
Swift 拥有泛型,协议,值类型以及闭包等特性,这些特性是对函数式⻛格的很好的介绍
术语
值(Value)
值是不变的永久的它不会发生改变,你如 1,2,3,true 等都是值这些事 字面量 值也可以是运行时生成的
var x = [1,2]
当使用一个值进行命名的时候 实际上创建了一个名为x的变量来持有[1,2]这个值,通过像是执行 x.append(3) 来改变x时我们并没有改变其原来的值,相反的是,我们所做的事使用[1,2,3,] 来改变x中的原有内容,可能实际上它的内部实现只是在某段内存的后面添加上一个条目,但是至少逻辑上来说值是全新的。我们将这个过程称为变量的改变
结构体(struct)和枚举(enum) 是值类型 当你把一个结构体变量赋值给另一个那么这两个变量将会包含同样的值。可以将它理解为内容被赋值了一遍。
引用 是一种特殊类型的值他是一个指向另一个值的值。两个引用可能会指向同一个值。这引入了一种可能性那就是这个值会被两个不同的部分所改变
类 (class) 是引用类型 (reference type)。你不能在一个变量里直接持有一个类的实例 (我们偶 尔可能会把这个实例称作对象 (object),这个术语经常被滥用,会让人困惑)。对于一个类的实 例,我们只能在变量里持有对它的引用,然后使用这个引用来访问它。
引用类型具有同一性 (identity),也就是说,你可以使用 === 来检查两个变量是否确实引用了 同一个对象。如果相应类型的 == 运算符被实现了的话,你也可以用 == 来判断两个变量是否 相等。两个不同的对象按照定义也是可能相等的。
值类型不存在同一性的问题。比如你不能对某个变量判定它是否和另一个变量持有 “相同” 的数 字 2。你只能检查它们都包含了 2 这个值。=== 运算符实际做的是询问 “这两个变量是不是持 有同样的引用”。在程序语言的论文里,== 有时候被称为结构相等,而 === 则被称为指针相等 或者引用相等。
深拷贝 对对象的地址进行拷贝
浅拷贝 之拷贝对象的指针
高阶函数 (higher-order function)。在 Swift 中,函数也是值。你可以将一个函数赋值给一个变量,也可以创建一个包含函数的数 组,或者调用变量所持有的函数。如果一个函数接受别的函数作为参数 (比如 map 函数接受一个转换函数,并将其应用到数组中的所有元素上),或者一个函数的返回值是函数,那么这样的 函数就叫做高阶函数 (higher-order function)。
闭包 (closure) block
内建集合类型
数组
在 Swift 中最常用的集合类型非数组莫属。数组是一系列相同类型的元素的有序的容器,对于 其中每个元素,我们可以使用下标对其直接进行访问 (这又被称作随机访问)。举个例子,要创 建一个数字的数组,我们可以这么写:
// 斐波那契数列
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]
区别使用 var 和 let 可以给我们带来不少好处。使用 let 定义的变量因为其具有不变性,因此更 有理由被优先使用。当你读到类似 let bs = ... 这样的声明时,你可以确定 bs 的值将永远不 变,这一点是由编译器强制保证的。这在你需要通读代码的时候会很有帮助。不过,要注意这只针对那些具有值语义的类型。使用 let 定义的类实例对象 (也就是说对于引用类型) 时,它保 证的是这个引用永远不会发生变化,你不能再给这个引用赋一个新的值,但是这个引用所指向 的对象却是可以改变的。
数组变形
对数组中的每个值执行转换操作是一个很常⻅的任务。每个程序员可能都写过上百次这样的代码:创建一个新数组,对已有数组中的元素进行循环依次取出其中元素,对取出的元素进行操作,并把操作的结果加入到新数组的末尾。比如,下面的代码计算了一个整数数组里的元素的平方:
var mutableFibs = [0, 1, 1, 2, 3, 5]
var squared: [Int] = []
for fib in fibs {
squared.append(fib * fib)
}
squared // [0, 1, 1, 4, 9, 25]
Swift 数组拥有 map 方法,这个方法来自函数式编程的世界。下面的例子使用了 map 来完成
同样的操作
let squares = mutableFibs.map { b in b * b }
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 将会把错误转发给调用者。
→ map和atMap—如何对元素进行变换
→ lter—元素是否应该被包含在结果中
→ reduce—如何将元素合并到一个总和的值中
→ sequence—序列中下一个元素应该是什么?
→ forEach—对于一个元素,应该执行怎样的操作
→ sort,lexicographicCompare和partition—两个元素应该以怎样的顺序进行排列 → index,rst和contains—元素是否符合某个条件
→ min和max—两个元素中的最小/最大值是哪个
→ elementsEqual和starts—两个元素是否相等
→ split—这个元素是否是一个分割符
所有这些函数的目的都是为了摆脱代码中那些杂乱无用的部分,比如像是创建新数组,对源数 据进行 for 循环之类的事情。这些杂乱代码都被一个单独的单词替代了。这可以重点突出那些 程序员想要表达的真正重要的逻辑代码。
这些函数中有一些拥有默认行为。除非你进行过指定,否则 sort 默认将会把可以作比较的元素 按照升序排列。contains 对于可以判等的元素,会直接检查两个元素是否相等。这些行为让代 码变得更加易读。升序排列非常自然,因此 array.sort() 的意义也很符合直觉。而对于 array.index(of: "foo") 这样的表达方式,也要比 array.index { $0 == "foo" } 更容易理解。
可变和带有状态的闭包
Filter
另一个常⻅操作是检查一个数组,然后将这个数组中符合一定条件的元素过滤出来并用它们创 建一个新的数组。对数组进行循环并且根据条件过滤其中元素的模式可以用数组的 lter 方法 表示:
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 和 lter,我们现在可以轻易完成很多数组操作,而不需要引入中间数组。 这会使得最终的代码变得更短更易读。比如,寻找 100 以内同时满足是偶数并且是其他数字的 平方的数,我们可以对 0..<10 进行 map 来得到所有平方数,然后再用 lter 过滤出其中的偶数
(1..<10).map { $0 * $0 }.fillter { $0 % 2 == 0 }
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 }
}
一个关于性能的小提示:如果你正在写下面这样的代码,请不要这么做!
bigArray.filter { someCondition }.count > 0
bigArray.contains { someCondition }
这种做法会比原来快得多,主要因为两个方面:它不会去为了计数而创建一整个全新的数组, 并且一旦匹配了第一个元素,它就将提前退出。一般来说,你只应该在需要所有结果时才去选 择使用 lter。
Reduce
map 和 lter 都作用在一个数组上,并产生另一个新的、经过修改的数组。不过有时候,你可 能会想把所有元素合并为一个新的值。比如,要是我们想将元素的值全部加起来,可以这样写:
var total = 0
for num in bs {
total = total + num
}
total // 12
reduce 方法对应这种模式,它把一个初始值 (在这里是 0) 以及一个将中间值 (total) 与序列中
的元素 (num) 进行合并的函数进行了抽象。使用 reduce,我们可以将上面的例子重写为这样:
let sum = mutableFibs.reduce(0) { (result, index) -> Int in
return result + index
}
// 12
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 lter2(_ isIncluded: (Element) -> Bool) -> [Element] { return reduce([]) {
isIncluded($1) ? $0 + [$1] : $0 }
}
}
这样的实现符合美学,并且不再需要那些啰嗦的命令式的 for 循环。但是 Swift 不是 Haskell, Swift 的数组并不是列表 (list)。在这里,每次执行 combine 函数都会通过在前面的元素之后附 加一个变换元素或者是已包含的元素,并创建一个全新的数组。这意味着上面两个实现的复杂 度是 O(n2),而不是 O(n)。随着数组⻓度的增加,执行这些函数所消耗的时间将以平方关系增 加。
时间复杂度是同一问题可用不同算法解决,而一个算法的质量优劣将影响到算法乃至程序的效率。算法分析的目的在于选择合适算法和改进算法。时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的,它考察当输入值大小趋近无穷时的情况。
空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度,记做S(n)=O(f(n))。比如直接插入排序的时间复杂度是O(n^2),空间复杂度是O(1) 。而一般的递归算法就要有O(n)的空间复杂度了,因为每次递归都要存储返回信息。一个算法的优劣主要从算法的执行时间和所需要占用的存储空间两个方面衡量。
flatMap
有时候我们会想要对一个数组用一个函数进行 map,但是这个变形函数返回的是另一个数组, 而不是单独的元素。
atMap 的实现看起来也和 map 基本一致,不过 atMap 需要的是一个能够返回数组的函数作
!"" !ags.characters.count // 1
let !ags = "
为变换参数。另外,在附加结果的时候,它使用的是 append(contentsOf:) 而不是 append(_:),
这样它将能把结果展平:
// the scalars are the underlying ISO country codes:
(!ags.unicodeScalars.map { String($0) }).joinWithSeparator(",") extension Array {
// # ,$ ,% ,&
func atMap(_ transform: (Element) -> [T]) -> [T] {
var result: [T] = [] ' forxinself{
result.append(contentsOf: transform(x))
( ".characters.count }
return result
}
"
atMap 的另一个常⻅使用情景是将不同数组里的元素进行合并。为了得到两个数组中元素的
所,有配对组合,我们可以对其中一个数组进行 atMap,然后对另一个进行 map 操作:
let suits = ["♠", "♥", "♣", "♦"]
let ranks = ["J","Q","K","A"]
let result = suits.atMap { suit in
ranks.map { rank in
(suit, rank)
}
}
forEach
我们最后要讨论的操作是 forEach。它和 for 循环的作为非常类似:传入的函数对序列中的每 个元素执行一次。和 map 不同,forEach 不返回任何值。技术上来说,我们可以不暇思索地将 一个 for 循环替换为 forEach:
for element in [1,2,3] {
print(element)
}
[1,2,3].forEach { element in
print(element)
}
不过,for 循环和 forEach 有些细微的不同,值得我们注意。比如,当一个 for 循环中有 return 语句时,将它重写为 forEach 会造成代码行为上的极大区别。让我们举个例子,下面的代码是 通过结合使用带有条件的 where 和 for 循环完成的:
extension Array where Element: Equatable { func index(of element: Element) -> Int? {
for idx in self.indices where self[idx] == element { return idx
}
return nil
}
}
我们不能直接将 where 语句加入到 forEach 中,所以我们可能会用 lter 来重写这段代码 (实 际上这段代码是错误的):
extension Array where Element: Equatable {
func index_foreach(of element: Element) -> Int? {
self.indices.lter { idx in self[idx] == element
}.forEach { idx in return idx
}
return nil
} }
在 forEach 中的 return 并不能返回到外部函数的作用域之外,它仅仅只是返回到闭包本身之 外,这和原来的逻辑就不一样了。在这种情况下,编译器会发现 return 语句的参数没有被使用, 从而给出警告,我们可以找到问题所在。但我们不应该将找到所有这类错误的希望寄托在编译 器上。
数组类型
切片
除了通过单独的下标来访问数组中的元素 (比如 bs[0]),我们还可以通过下标来获取某个范围 中的元素。比如,想要得到数组中除了首个元素的其他元素,我们可以这么做:
let slice = bs[1..
它将返回数组的一个切片 (slice),其中包含了原数组中从第二个元素到最后一个元素的数据。 得到的结果的类型是 ArraySlice,而不是 Array。切片类型只是数组的一种表示方式,它背后的 数据仍然是原来的数组,只不过是用切片的方式来进行表示。这意味着原来的数组并不需要被 复制。ArraySlice 具有的方法和 Array 上定义的方法是一致的,因此你可以把它们当做数组来 进行处理。如果你需要将切片转换为数组的话,你可以通过将切片传递给 Array 的构建方法来 完成: Swift 中另一个关键的数据结构是 Dictionary,字典。字典包含键以及它们所对应的值。在一个 字典中,每个键都只能出现一次。通过键来获取值所花费的平均时间是常数量级的 (作为对比, 在数组中搜寻一个特定元素所花的时间将与数组尺寸成正比)。和数组有所不同,字典是无序 的,使用 for 循环来枚举字典中的键值对时,顺序是不确定的。 和数组一样,使用 let 定义的字典是不可变的:你不能向其中添加、删除或者修改条目。如果想 要定义一个可变的字典,你需要使用 var 进行声明。想要将某个值从字典中移除,可以用下标 将对应的值设为 nil,或者调用 removeValue(forKey:)。后一种方法除了删除这个键以外,还会 将被删除的值返回 (如果待删除的键不存在,则返回 nil)。对于一个不可变的字典,想要进行改 变的话,首先需要进行复制: 我们扩展 Dictionary 类型,为它添加一个 merge 方法,该方法接受待合并的字典作为参数。我 们可以将这个参数指明为 Dictionary 类型,不过更好的选择是用更加通用的泛型方法来进行实 现。我们对参数的要求是,它必须是一个序列,这样我们就可以对其进行循环枚举。另外,序 列的元素必须是键值对,而且它必须和接受方法调用的字典的键值对拥有相同类型。对于任意 的 Sequence,如果它的 Iterator.Element 是 (Key, Value) 的话,它就满足我们的要求,因此 我们将其作为泛型的约束 (这里的 Key 和 Value 是我们所扩展的 Dictionary 中已经定义的泛型 类型参数): 字典其实是哈希表。字典通过键的 hashValue 来为每个键指定一个位置,以及它所对应的存 标准库中第三种主要的集合类型是集合 Set (虽然听起来有些别扭)。集合是一组无序的元素, 每个元素只会出现一次。你可以将集合想像为一个只存储了键而没有存储值的字典。和 Dictionary 一样,Set 也是通过哈希表实现的,并拥有类似的性能特性和要求。测试集合中是 否包含某个元素是一个常数时间的操作,和字典中的键一样,集合中的元素也必须满足 Hashable。如果你需要高效地测试某个元素是否存在于序列中并且元素的顺序不重要时,使用集合是更好 的选择 (同样的操作在数组中的复杂度是 O(n))。另外,当你需要保证序列中不出现重复元素 时,也可以使用集合。 正如其名,集合 Set 和数学概念上的集合有着紧密关系;Set 也支持你在高中数学中学到的那 些基本集合操作。比如,我们可以在一个集合中求另一个集合的补集: 我们也可以求两个集合的交集,找出两个集合中都含有的元素: 或者,我们能求两个集合的并集,将两个集合合并为一个 (当然,移除那些重复多余的): 就算不暴露给函数的调用者,字典和集合在函数中也会是非常好用的数据结构。我们如果想要 为 Sequence 写一个扩展,来获取序列中所有的唯一元素,我们只需要将这些元素放到一个 Set 里,然后返回这个集合的内容就行了。不过,因为 Set 并没有定义顺序,所以这么做是不 稳定的,输入的元素的顺序在结果中可能会不一致。为了解决这个问题,我们可以创建一个扩 展来解决这个问题,在扩展方法内部我们还是使用 Set 来验证唯一性: 上面这个方法让我们可以找到序列中的所有不重复的元素,并且维持它们原来的顺序。在我们 传递给 lter 的闭包中,我们使用了一个外部的 seen 变量,我们可以在闭包里访问和修改它的 值。我们会在函数一章中详细讨论它背后的技术。 范围代表的是两个值的区间,它由上下边界进行定义。你可以通过 ..< 来创建一个不包含上边界 的半开范围,或者使用 ... 创建同时包含上下边界的闭合范围:
Array(fibs[1..字典
我们使用下标的方式可以得到某个设置的值 (比如 defaultSettings["Name"])。字典查找将返回 的是可选值,当特定键不存在时,下标查询返回 nil。这点和数组有所不同,在数组中,使用越 界下标进行访问将会导致程序崩溃。可变性
有用的字典扩展
extension Dictionary {
mutating func merge
(_ other: S)
where S: Sequence, S.Iterator.Element == (key: Key, value: Value) { for (k, v) in other {
self[k] = v }
} }
Hashable 要求
储。这也就是 Dictionary 要求它的 Key 类型需要遵守 Hashable 协议的原因。标准库中所有的 基本数据类型都是遵守 Hashable 协议的,它们包括字符串,整数,浮点数以及布尔值。不带 有关联值的枚举类型也会自动遵守 Hashable。
如果你想要将自定义的类型用作字典的键,那么你必须手动为你的类型添加 Hashable 并满足 它,这需要你实现 hashValue 属性。另外,因为 Hashable 本身是对 Equatable 的扩展,因此 你还需要为你的类型重载 == 运算符。你的实现必须保证哈希不变原则:两个同样的实例 (由你 实现的 == 定义相同),必须拥有同样地哈希值。不过反过来不必为真:两个相同哈希值的实例 不一定需要相等。不同的哈希值的数量是有限的,然而很多可以被哈希的类型 (比如字符串) 的 个数是无穷的。
哈希值可能重复这一特性,意味着 Dictionary 必须能够处理哈希碰撞。不必说,优秀的哈希算 法总是能给出较少的碰撞,这将保持集合的性能特性。理想状态下,我们希望得到的哈希值在整个整数范围内平均分布。在极端的例子下,如果你的实现对所有实例返回相同的哈希值 (比如 0),那么这个字典的查找性能将下降到 O(n)。
优秀哈希算法的第二个特质是它应该很快。记住,在字典中进行插入,移除,或者查找时,这 些哈希值都要被计算。如果你的 hashValue 实现要消耗太多时间,那么它很可能会拖慢你的程 序,让你从字典的 O(1) 特性中得到的好处损失殆尽。Set集合
集合代数
let iPods: Set = ["iPod touch", "iPod nano", "iPod mini", "iPod shufe", "iPod Classic"]
let discontinuedIPods: Set = ["iPod mini", "iPod Classic"] let currentIPods = iPods.subtracting(discontinuedIPods) // ["iPod shufe", "iPod nano", "iPod touch"]
let touchscreen: Set = ["iPhone", "iPad", "iPod touch", "iPod nano"] let iPodsWithTouch = iPods.intersection(touchscreen)
// ["iPod touch", "iPod nano"]
var discontinued: Set = ["iBook", "Powerbook", "Power Mac"] discontinued.formUnion(discontinuedIPods)
// ["iBook", "iPod mini", "Powerbook", "Power Mac", "iPod Classic"]
索引集合和字符集合
在闭包中使用集合
extension Sequence where Iterator.Element: Hashable { func unique() -> [Iterator.Element] {
var seen: Set
Range
// 0 到 9, 不包含 10
let singleDigitNumbers = 0..<10
// 包含 "z"
let lowercaseLetters = Character("a")...Character("z")