Swift:Array
对任何一门现代化编程语言来说,集合类型都是非常重要的组成部分。这一类数据类型的设计,很大程度上决定了开发者对某种编程语言的使用体验以及代码执行效率。因此,Swift 在集合类型的设计和实现上,进行了诸多的考量,让它兼具易用性、高性能以及扩展性。
但是这样做也是有代价的,在 Swift 里,集合是个复杂的类型家族,它是由多个 protocol 形成的,因此当我们想深入其中一探究竟的时候,并不那么容易。
最简单也最常用的一个集合类型:Array。
创建一个 Array
Array 表示一组有序(ordered)的数据集合,所谓有序,并不是指大小排序,而是指 Array 中的元素有先后的位置关系,稍后我们会看到,这个位置关系可以用来访问 Array 中的元素。在此之前,先来看了解如何定义 Array 对象。
首先,我们可以通过下面三种方法定义一个空的 Array:
var array1: Array<Int> = Array<Int>() |
在上面的代码中,前两种使用了 type annotation,Array
其次,再来看一些定义数组时同时指定初始值的方法:
// [3, 3, 3] |
它们用起来都很直观,在稍后我们提到 Sequence 时,还会看到更复杂的 Array 初始化方法。
两个常用的 Array 属性(isEmpty,count)
定义好数组之后,我们介绍两个 Array 最常用的属性。第一个是 count,类型是 Int。我们之前已经用过,用于获取数组中元素的个数:
array1.count // 0 |
第二个是 isEmtpy,类型是 Bool。表示数组是否为空:
if array2.isEmpty { |
访问 Array 中的元素
接下来,我们看访问 Array 元素的方法,它们之中有我们在其他语言中熟悉的,也有 Swift 独特的方式。首先,就是几乎所有语言都有的惯用法,使用索引。但是,它却也是在 Swift,最不被推荐的使用方法:
fiveInts[2] // 3 |
就像,上面例子中这样。当使用索引访问数组元素时,你必须自行确保索引的安全性。如果索引超过了数组的范围,程序就会直接崩溃。其实,在 Swift 里,我们几乎不需要直接使用索引来访问数组元素。稍后,我们会专门提到 Array 的惯用法。因此,Swift 开发者也没有对索引访问添加任何安全保护。言外之意就是,非要用,你自己对结果全权负责喽。
其次,是使用 range operator 访问数组的一个范围:
fiveInts[0...2] // [1, 2, 3] |
要说明的是,使用 range operator 得到的,并不是一个 Array,而是一个 ArraySlice。什么是 ArraySlice 呢?简单来说,就是 Array 某一段内容的 view,它不真正保存数组的内容,只保存这个 view 引用的数组的范围:
// +---------+---+ |
从上面这个注释,就很容易理解 view 的概念了,它只记录了要表达内容的区间。但是我们也可以直接通过这个 view 创建新的 Array 对象:
Array(fiveInts[0...2]) // [1, 2, 3] |
这样,就得到了一个值是 [1, 2, 3] 的 Array 对象。
遍历数组
除了访问单个元素外,另一类常用的需求就是顺序访问数组中的每个成员。在 Swift 里,我们有三种基本的方法遍历一个 Array。
For(不推荐)
for value in fiveInts { |
enumerated ()(不推荐)
在遍历的时候,同时获得数组的索引和值,可以使用数组对象的 enumerated () 方法,它会返回一个 Sequence 对象,包含了每个成员的索引和值,我们同样可以在 for 循环中,依次访问它们:
for (index, value) in fiveInts.enumerated() { |
当我们要查找数组中元素的位置时(例如,查找等于 1 的元素的索引):
a.index { $0 == 1 } |
index 会返回一个 Optional
当我们要筛选出数组中的某些元素时(例如,得到所有偶数):
a.filter { $0 % 2 == 0 } |
forEach
借助 closure,可以使用 Array 对象的 forEach 方法
要注意它和 map 的一个重要区别:forEach 并不处理 closure 参数的返回值。因此它只适合用来对数组中的元素进行一些操作,而不能用来产生返回结果。
fiveInts.forEach { print($0) } |
map
map 把 for 循环执行的逻辑,封装在了函数里,这样我们就可以把函数的返回值赋值给常量了
var fibonacci = [0, 1, 1, 2, 3, 5] |
例如我们可以实现自己的 map 方法
extension Array { |
仔细观察 myMap 的实现,就会发现它最大的意义,就是保留了遍历 Array 的过程,而把要执行的动作留给了 myMap 的调用者通过参数去定制
print(fibonacci.map { $0 }) // [0, 1, 1, 2, 3, 5] |
Array 其他的方法
flatMap
简单来说,如果你用在 map 中的 closure 参数不返回一个数组元素,而是也返回一个数组,这样,你就会得到一个数组的数组,但如果你只需要一个一维数组,flatMap 就可以派上用场了
var animal = ["cat","dog"] |
实际上,flatMap 的实现很简单,只要在 map 内部的 for 循环里,不断把 closure 参数生成的数组的内容,添加到要返回的结果里就好了:
extension Array { |
得到的结果,应该和之前使用 flatMap 是一样的:
min () max ()(返回最大最小值)
var fibonacci = [0, 1, 1, 2, 3, 5] |
filter(返回判断正确的值)
var fibonacci = [0, 1, 1, 2, 3, 5] |
我们可以自己来实现一个 filter:
extension Array { |
- myFilter:最核心的环节就是通过带有 where 条件的 for 循环找到原数组中符合条件的元素,然后把它们一一添加到 tmp 中,并最终返回给函数的调用者。然后,我们测试下 myFilter
- reject:剔除掉数组中满足条件的元素
- allMatch:基于这个 contains,我们还可以给 Array 添加一个新的方法,用来判断 Array 中所有的元素是否满足特定的条件:
var fibonacci = [0, 1, 1, 2, 3, 5] |
contains(是否存在满足条件的元素)
fibonacci.contains { $0 % 2 == 0 } // true |
contains 的一个好处就是只要遇到满足条件的元素,函数的执行就终止了
elementsEqual () starts ()(比较数组)
比较数组相等或以特定元素开始。对这类操作,我们需要提供两个内容
- elementsEqual:比较数组元素是否完全相等
- starts:比较数组的规则是否以特定序列开头
var fibonacci = [0, 1, 1, 2, 3, 5] |
sorted ()(对数组进行排序)
// [0, 1, 1, 2, 3, 5] |
其中,sorted (by:) 的用法是很直接的,它默认采用升序排列。同时,也允许我们通过 by 自定义排序规则。在这里 > 是 { $0 > $1 } 的简写形式。Swift 中有很多在不影响语义的情况下的简写形式。
而 partition (by:) 则会先对传递给它的数组进行重排,然后根据指定的条件在重排的结果中返回一个分界点位置。这个分界点分开的两部分中,前半部分的元素都不满足指定条件;后半部分都满足指定条件。而后,我们就可以使用 range operator 来访问这两个区间形成的 Array 对象。大家可以根据例子中注释的结果,来理解 partition 的用法。
reduce ()(对数组所有内容合并)
是把数组的所有内容,“合并” 成某种形式的值,对这类操作,我们需要指定的,是合并前的初始值,以及 “合并” 的规则。例如,我们计算 fibonacci 中所有元素的和:
fibonacci.reduce(0, +) // 12 |
在这里,初始值是 0,和第二个参数 +,则是 {$0 + $1} 的缩写。
了解 reduce 的进一步用法之前,我们先来自己实现一个:
extension Array { |
它们的结果和标准库中的 map 和 filter 是一样的。但是,这种看似优雅的写法却没有想象中的那么好。在它们内部的 reduce 调用中,每一次 $0 的参数都是一个新建的数组,因此整个算法的复杂度是 O (n2),而不再是 for 循环版本的 O (n)。所以,这样的实现方法最好还是用来作为理解 reduce 用法的例子。
append(在末尾添加)
编辑 Array 中的元素。要在数组的末尾添加元素,我们可以这样:
array1.append(1) // [1] |
insert(在中间位置添加)
它的第一个参数表示要插入的值,第二个参数表示要插入的位置,这个位置必须是一个合法的范围,即 0…array1.endIndex,如果超出这个范围,会直接引发运行时错误。
// [1, 2, 3, 4, 5] |
- endIndex 末尾
- startIndex 起始位置
- array1 [N] 指定位置
remove (at:)(删除元素的位置)
要删除 Array 中的元素,可以使用 remove (at:) 方法,它只接受一个参数,表示要删除元素的位置,同样,你必须自行保证使用的 at 参数不超过数组的合法范围,否则会引发运行时错误。当然,如果你仅仅想删除数组中的最后一个元素,还可以使用 removeLast () 方法:
array1.remove(at: 4) // [1, 2, 3, 4] |
其他方法
array1.removeLast() // [1, 2, 3] |
- removeAll 删除全部元素 //[]
- removeFirst 删除第一个元素 [2,3,4]
- removeLast 删除最后一个元素 [1,2,3]
- popLast 删除的最后一个元素(原 Array 已被删除) [4]
NSArray
在 Foundation 中,数组这个类型有两点和 Swift Array 是不同的:
- 数组是否可以被修改是通过 NSArray 和 NSMutableArray 这两个类型来决定的;
- NSArray 和 NSMutableArray 都是类对象,复制它们执行的是引用语义;
当把这两个因素放在一起的时候,Foundation 中的 “常量数组” 这个概念就会在一些场景里表现的很奇怪。因为你还可以通过对一个常量数组的非常量引用去修改它,来看下面的例子:
// Mutable array [1, 2, 3] |
从上面的代码可以看到,尽管我们在创建 copyB 时,使用了 NSArray,表明我们不希望它的值被修改,由于这个赋值执行的是引用拷贝,因此,实际上它和 b 指向的是同一块内存空间。因此,当我们修改 b 的内容时,copyB 也就间接受到了影响。
为了在拷贝 NSArray 对象时,执行值语义,我们必须使用它的 copy 方法复制所有的元素:
let b = NSMutableArray(array: [1, 2, 3]) |
从注释中的结果,你就能很容易理解 deep copy 的含义了。
当我们使用 NSArray 和 NSMutableArray 时,Swift 中的 var 和 let 关键字就和数组是否可以被修改没关系了。它们只控制对应的变量是否可以被赋值成新的 NSArray 或 NSMutableArray 对象。