Swift官方API文档

对任何一门现代化编程语言来说,集合类型都是非常重要的组成部分。这一类数据类型的设计,很大程度上决定了开发者对某种编程语言的使用体验以及代码执行效率。因此,Swift在集合类型的设计和实现上,进行了诸多的考量,让它兼具易用性、高性能以及扩展性。

但是这样做也是有代价的,在Swift里,集合是个复杂的类型家族,它是由多个protocol形成的,因此当我们想深入其中一探究竟的时候,并不那么容易。

最简单也最常用的一个集合类型:Array。

创建一个Array

Array表示一组有序(ordered)的数据集合,所谓有序,并不是指大小排序,而是指Array中的元素有先后的位置关系,稍后我们会看到,这个位置关系可以用来访问Array中的元素。在此之前,先来看了解如何定义Array对象。
首先,我们可以通过下面三种方法定义一个空的Array:

var array1: Array<Int> = Array<Int>()
var array2: [Int] = []
var array3 = array2

在上面的代码中,前两种使用了type annotation,Array和[Int]没有区别,你可以根据自己的喜好来选择。而第三种,我们直接使用了一个空的Array生成了一个新的Array对象。

其次,再来看一些定义数组时同时指定初始值的方法:

// [3, 3, 3]
var threeInts = [Int](repeating: 3, count: 3)
// [3, 3, 3, 3, 3, 3]
var sixInts = threeInts + threeInts
// [1, 2, 3, 4, 5]
var fiveInts = [1, 2, 3, 4, 5]

它们用起来都很直观,在稍后我们提到Sequence时,还会看到更复杂的Array初始化方法。

两个常用的Array属性(isEmpty,count)

定义好数组之后,我们介绍两个Array最常用的属性。第一个是count,类型是Int。我们之前已经用过,用于获取数组中元素的个数:

array1.count    // 0
fiveInts.count // 5

第二个是isEmtpy,类型是Bool。表示数组是否为空:

if array2.isEmpty {
// array2 is empty
print("array2 is empty")
}

访问Array中的元素

接下来,我们看访问Array元素的方法,它们之中有我们在其他语言中熟悉的,也有Swift独特的方式。首先,就是几乎所有语言都有的惯用法,使用索引。但是,它却也是在Swift,最不被推荐的使用方法:

fiveInts[2] // 3
fiveInts[5] // This will crash

就像,上面例子中这样。当使用索引访问数组元素时,你必须自行确保索引的安全性。如果索引超过了数组的范围,程序就会直接崩溃。其实,在Swift里,我们几乎不需要直接使用索引来访问数组元素。稍后,我们会专门提到Array的惯用法。因此,Swift开发者也没有对索引访问添加任何安全保护。言外之意就是,非要用,你自己对结果全权负责喽。

其次,是使用range operator访问数组的一个范围:

fiveInts[0...2] // [1, 2, 3]
fiveInts[0..<2] // [1, 2]

要说明的是,使用range operator得到的,并不是一个Array,而是一个ArraySlice。什么是ArraySlice呢?简单来说,就是Array某一段内容的view,它不真正保存数组的内容,只保存这个view引用的数组的范围:

// +---------+---+
// | length | 5 |
// +---------+---+
// | storage ptr |
// +---------+---+
// |
// v
// +---+---+---+---+---+---------------------+
// | 1 | 2 | 3 | 4 | 5 | reserved capacity |
// +---+---+---+---+---+---------------------+
// ^
// |
// +---------+---+
// | storage ptr |
// +---------+---+
// | beg idx | 0 |
// +---------+---+
// | end idx | 3 | ArraySlice for [0...2]
// +---------+---+

从上面这个注释,就很容易理解view的概念了,它只记录了要表达内容的区间。但是我们也可以直接通过这个view创建新的Array对象:

Array(fiveInts[0...2]) // [1, 2, 3]

这样,就得到了一个值是[1, 2, 3]的Array对象。

遍历数组

除了访问单个元素外,另一类常用的需求就是顺序访问数组中的每个成员。在Swift里,我们有三种基本的方法遍历一个Array。

For(不推荐)

for value in fiveInts {
print(value)
}
// 1
// 2
// ...

/* or */
a.forEach { print($0) }
// 1
// 2
// ...

enumerated()(不推荐)

在遍历的时候,同时获得数组的索引和值,可以使用数组对象的enumerated()方法,它会返回一个Sequence对象,包含了每个成员的索引和值,我们同样可以在for循环中,依次访问它们:

for (index, value) in fiveInts.enumerated() {
print("\(index): \(value)")
}
// 0: 1
// 1: 2
// ...

当我们要查找数组中元素的位置时(例如,查找等于1的元素的索引):

a.index { $0 == 1 }

index会返回一个Optional,当要查找的元素存在时,就返回该元素的索引,否则,就返回nil。

当我们要筛选出数组中的某些元素时(例如,得到所有偶数):

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]
var squares = [Int]()

for value in fibonacci {
squares.append(value * value)
}
//等同于下面方法
let constSquares = fibonacci.map { $0 * $0 }

例如我们可以实现自己的map方法

extension Array {
func myMap<T>(_ transform: (Element) -> T) -> [T] {
var tmp: [T] = []
tmp.reserveCapacity(count)

for value in self {
tmp.append(transform(value))
}

return tmp
}
}
let constSequence1 = fibonacci.myMap { $0 * $0 }

仔细观察myMap的实现,就会发现它最大的意义,就是保留了遍历Array的过程,而把要执行的动作留给了myMap的调用者通过参数去定制

print(fibonacci.map { $0 })   // [0, 1, 1, 2, 3, 5]
print(fibonacci.map { $0 * $0 }) //[0, 1, 1, 4, 9, 25]

Array 其他的方法

flatMap

简单来说,如果你用在map中的closure参数不返回一个数组元素,而是也返回一个数组,这样,你就会得到一个数组的数组,但如果你只需要一个一维数组,flatMap就可以派上用场了

var animal = ["cat","dog"]
var ids = ["1","2"]
animal.flatMap { animal in
return ids.map { id in (animal, id) } // [(.0 "cat", .1 "1"), (.0 "cat", .1 "2"), (.0 "dog", .1 "1"), (.0 "dog", .1 "2")]
}

实际上,flatMap的实现很简单,只要在map内部的for循环里,不断把closure参数生成的数组的内容,添加到要返回的结果里就好了:

extension Array {
func myFlatMap<T>(_ transform: (Element) -> [T]) -> [T] {
var tmp: [T] = []

for value in self {
tmp.append(contentsOf: transform(value))
}

return tmp
}
}

得到的结果,应该和之前使用flatMap是一样的:

min() max()(返回最大最小值)

var fibonacci = [0, 1, 1, 2, 3, 5]
fibonacci.min() // 0
fibonacci.max() // 5

filter(返回判断正确的值)

var fibonacci = [0, 1, 1, 2, 3, 5]
fibonacci.filter { $0 % 2 == 0 } //[0, 2]
fibonacci.map { $0 % 2 == 0 } //[true, false, false, true, false, false]

我们可以自己来实现一个filter:

extension Array {
func myFilter(_ predicate: (Element) -> Bool) -> [Element] {
//一个参数predicate用来设置筛选的条件,这个条件接受一个Element类型的参数,返回一个Bool值,最后让myFilter返回一个Element值
var tmp: [Element] = [] //定义一个空的数组用来存放筛选后的结果

for value in self where predicate(value) { //使用for循环,便利数组中的每一个元素
tmp.append(value) //把符合条件的值添加到tmp里面
}
return tmp
}
func reject(_ predicate: (Element) -> Bool) -> [Element] {
return filter { !predicate($0) } //指定的条件取反
}
func allMatch(_ predicate: (Element) -> Bool) -> Bool {
return !contains { !predicate($0) }
}
}
  • myFilter:最核心的环节就是通过带有where条件的for循环找到原数组中符合条件的元素,然后把它们一一添加到tmp中,并最终返回给函数的调用者。然后,我们测试下myFilter
  • reject:剔除掉数组中满足条件的元素
  • allMatch:基于这个contains,我们还可以给Array添加一个新的方法,用来判断Array中所有的元素是否满足特定的条件:
var fibonacci = [0, 1, 1, 2, 3, 5]
fibonacci.myFilter { $0 % 2 == 0 } //[0, 2]
fibonacci.reject { $0 % 2 == 0 } // [1, 1, 3, 5]
//我们只要把调用转发给filter,然后把指定的条件取反就好了。这样,剔除元素的代码语义上就会更好看一些:

let evens = [2, 4, 6, 8]
evens.allMatch { $0 % 2 == 0 } // true
//在allMatch的实现里,只要没有不满足条件的元素,也就是所有元素都满足条件了

contains(是否存在满足条件的元素)

fibonacci.contains { $0 % 2 == 0 } // true

contains的一个好处就是只要遇到满足条件的元素,函数的执行就终止了

elementsEqual() starts()(比较数组)

比较数组相等或以特定元素开始。对这类操作,我们需要提供两个内容

  • elementsEqual:比较数组元素是否完全相等
  • starts:比较数组的规则是否以特定序列开头
var fibonacci = [0, 1, 1, 2, 3, 5]
fibonacci.elementsEqual([0, 1, 1], by: { $0 == $1 }) //false
fibonacci.starts(with: [0, 1, 1], by: { $0 == $1 }) //true

sorted()(对数组进行排序)

// [0, 1, 1, 2, 3, 5]
fibonacci.sorted()
// [5, 3, 2, 1, 1, 0]
fibonacci.sorted(by: >)

let pivot = fibonacci.partition(by: { $0 < 1 })
fibonacci[0 ..< pivot] // [5, 1,1,2, 3]
fibonacci[pivot ..< fibonacci.endIndex] // [0]

其中,sorted(by:)的用法是很直接的,它默认采用升序排列。同时,也允许我们通过by自定义排序规则。在这里>是{ $0 > $1 }的简写形式。Swift中有很多在不影响语义的情况下的简写形式。

而partition(by:)则会先对传递给它的数组进行重排,然后根据指定的条件在重排的结果中返回一个分界点位置。这个分界点分开的两部分中,前半部分的元素都不满足指定条件;后半部分都满足指定条件。而后,我们就可以使用range operator来访问这两个区间形成的Array对象。大家可以根据例子中注释的结果,来理解partition的用法。

reduce()(对数组所有内容合并)

是把数组的所有内容,“合并”成某种形式的值,对这类操作,我们需要指定的,是合并前的初始值,以及“合并”的规则。例如,我们计算fibonacci中所有元素的和:

fibonacci.reduce(0, +)   // 12

在这里,初始值是0,和第二个参数+,则是{ $0 + $1 }的缩写。

了解reduce的进一步用法之前,我们先来自己实现一个:

extension Array {
func myReduce<T>(_ initial: T, _ next: (T, Element) -> T) -> T {
//由于要把Array转化成某种形式的单一值,把它定义范型方法,按照用法应该有两个参数,第一个参数initial是Reduce的初始值,定义为T;第二个表示每次执行的方法,是一个clouser,有两个参数T, Element,并且返回合并后的结果T,最终myReduce返回T集合
var tmp = initial

for value in self {
tmp = next(tmp, value)
}

return tmp
}
}

fibonacci.myReduce(0, +) // 12

它们的结果和标准库中的map和filter是一样的。但是,这种看似优雅的写法却没有想象中的那么好。在它们内部的reduce调用中,每一次$0的参数都是一个新建的数组,因此整个算法的复杂度是O(n2),而不再是for循环版本的O(n)。所以,这样的实现方法最好还是用来作为理解reduce用法的例子。

append(在末尾添加)

编辑Array中的元素。要在数组的末尾添加元素,我们可以这样:

array1.append(1)     // [1]
array1 += [2, 3, 4] // [1, 2, 3, 4]

insert(在中间位置添加)

它的第一个参数表示要插入的值,第二个参数表示要插入的位置,这个位置必须是一个合法的范围,即0…array1.endIndex,如果超出这个范围,会直接引发运行时错误。

// [1, 2, 3, 4, 5]
array1.insert(5, at: array1.endIndex)
  • 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]
let b = NSMutableArray(array: [1, 2, 3])
// Const array [1, 2, 3]
let copyB: NSArray = b

// [0, 1, 2, 3]
b.insert(0, at: 0)
// [0, 1, 2, 3]
copyB

从上面的代码可以看到,尽管我们在创建copyB时,使用了NSArray,表明我们不希望它的值被修改,由于这个赋值执行的是引用拷贝,因此,实际上它和b指向的是同一块内存空间。因此,当我们修改b的内容时,copyB也就间接受到了影响。

为了在拷贝NSArray对象时,执行值语义,我们必须使用它的copy方法复制所有的元素:

let b = NSMutableArray(array: [1, 2, 3])
let copyB: NSArray = b
let deepCopyB = b.copy() as! NSArray

b.insert(0, at: 0) // [0, 1, 2, 3]
copyB // [0, 1, 2, 3]
deepCopyB // [1, 2, 3]

从注释中的结果,你就能很容易理解deep copy的含义了。

当我们使用NSArray和NSMutableArray时,Swift中的var和let关键字就和数组是否可以被修改没关系了。它们只控制对应的变量是否可以被赋值成新的NSArray或NSMutableArray对象。