Swift进阶-更高效的使用集合

集合是极大简化我们工作效率的一种类型,我们可以通过它存储同类型的多条数据(Array);可以存储同类型且唯一的多条数据(Set);也可以存储多条键值对(Dictionary)。学会高效的使用集合,不仅可以提高我们代码的性能,还可以极大地节约我们的开发时间。

下面,让我们开始吧!

获取集合的元素

获取第一个元素

在日常开发中,我们会经常获取数组的元素,那么我们应该如何获取集合的第一个元素呢?

可以通过以下三种方式:

  • 通过下标:arr[0]
  • 通过index:arr[arr.startIndex]
  • 通过调用: first

通过下标:我们只能应用于 Array ,对于 Set 或者 Dictionary 是不能用的,而且如果数组为空会 crash。

通过index:可以应用于 Array 、 Set 、 Dictionary 。但数组为空也会 crash。

通过 first :可以应用于 Array 、 Set 、 Dictionary 。而且它返回的是一个可选值,如果集合为空不会 crash。

结论:通过上面的对比,我们知道应该使用 first 来安全的获取第一个元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let nums = Array(1...10)
if let first = nums.first {
print(first)
}
// 因为 Set 和Dictionary 是无序的,所以打印的第一个元素只是本次存储空间第一个位置的元素,多次运行结果会不一致。
let set: Set = [1,2,3,4]
if let first = set.first {
print(first)
}
let dict = ["name": "fzh", "age": "18"]
if let first = dict.first {
print(first)
}

说完了安全的获取第一个元素,下面我们来看一下如何获取第二个元素(放心,不会有如何获取第三个元素啦)。

获取第二个元素

首先,下标的方式是肯定不行的,我们来看使用 index 的方式如何获取集合的第二个元素:

1
2
3
4
5
6
7
8
9
10
11
12
extension Collection {
var second: Element? {
//集合是否为空
guard self.startIndex != self.endIndex else { return nil}
//获取第二个元素的index
let index = self.index(after: self.startIndex)
//index 是否有效
guard index != self.endIndex else { return nil }
//返回第二个元素
return self[index]
}
}

可以看出,上面的代码看着还是比较多的。这种情况下,我们可能会想到标准库是否有 second 属性?但是很抱歉,没有。那么我们有简化上面的代码的方式吗?这个是可以有的。我们可以通过切片(Slice)的方式来获取。

1
2
3
4
5
6
extension Collection {
var second: Element? {
// dropFirst():丢掉第一个元素 生成一个切片;再访问切片的第一个元素,也就是 second 了
return self.dropFirst().first
}
}

slice

切片(Slice)

切片是一个集合的子集。它主要用来进行一些临时的操作,比如获取连续的几个元素:

1
2
3
4
// 获取前半段的元素
var array = Array(1...8)
var firstHalf = array.dropLast(array.count/2)
print(firstHalf) // [1, 2, 3, 4]

新建一个切片,并不会新建内存地址,将原集合的元素拷贝过来。它只是指向原集合的一个指针,因此创建一个slice 复杂度是O(1)。

而且,只要原集合没有改变,切片和集合的索引就是一致的。

1
2
3
4
print(array[2], firstHalf[2]) // 3 3
// 原集合改变,不影响已生成的切片
array[2] = -1
print(array[2], firstHalf[2]) // -1 3

如果我们将切片转换为数组的话,就会新建内存地址将切片范围中的元素拷贝到新的内存地址:

1
2
3
// 这一句会开辟内存,将值拷贝过来
let copy = Array(firstHalf)
print(copy) // [1, 2, 3, 4]

slice2.png

我们应该只在进行临时操作的时候使用切片,因为即使原集合的生命周期已经结束,切片还是可能对原集合强引用。因此,长时间使用切片可能会导致内存泄漏。想详细了解切片的请移步此处

Note

我们在使用切片时,要注意索引的问题,比如下面的代码就会 crash :

1
2
3
var array = Array(1...8)
let slice = array[2...5]
print(slice[0])

对于上述例子,不要使用 slice[0] 来获取切片的第一个元素,因为我们的切片的索引只包含[2...5],所以调用 slice[0] 会 crash。我们可以将它转为数组,再用 [0] 获取第一个元素:

1
2
var subArr = Array(slice)
print(subArr[0]) // 3

Lazy Function

在我们对集合进行运算的时候,我们可能会写下面的代码:

1
2
3
let items = (1...4000).map { $0 * 2 }.filter { return $0 < 10 }
print(items.first) // Optional(2)

虽然我们可能需要的只是 items 的第一个元素,但是代码还是会对集合的所有元素进行 map 和 filter 操作。

那么,我们如何能只对想要的元素进行map 和 filter 操作呢?答案就是 lazy Function。我们可以将上面的代码改为下面的样子:

1
2
3
let items = (1...4000).lazy.map { $0 * 2 }.filter { return $0 < 10 }
print(items.first) // Optional(2)

什么时候使用

  • 在进行链式运算的时候
  • 只需要结果中的一部分元素
  • 没有副作用

如何有效的避免集合 crash

是否在改变集合

下面是改变集合发生 crash 的一个例子:

1
2
3
4
var array = ["A", "B", "C", "D", "E"]
let index = array.firstIndex(of: "E")!
array.remove(at: array.startIndex)
print(array[index])

因为在执行 remove 操作后,数组的长度发生了变化,而 index 恰好是原数组的最后一个元素,所以会数组越界。

我们可以通过下面这种方式,安全的使用集合:

1
2
3
4
5
var array = ["A", "B", "C", "D", "E"]
array.remove(at: array.startIndex)
if let index = array.firstIndex(of: "E") {
print(array[index]) // E
}

我们只要记住一点就可以避免上面的问题:要在改变集合之后在获取 index 来得到数据

是否多线程访问集合

集合为了优化性能默认是单线程访问的,如果多条线程同时访问集合而又没有添加锁或者使用串行队列的话可能会发生出人意料的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
// thread 1
var sleepingBeears = [String]()
let queue = DispatchQueue.global()
// thread 2
queue.async {
sleepingBeears.append("Grandpa")
}
// thread 3
queue.async {
sleepingBeears.append("Cub")
}
// thread 4
print(sleepingBeears)

上面的代码会产生如下的结果:

error

我们可以使用串行队列来避免上面的问题:

1
2
3
4
5
6
7
8
var sleepingBeears = [String]()
let queue = DispatchQueue(label: "Bear-Cave")
queue.async { sleepingBeears.append("Grandpa") }
queue.async { sleepingBeears.append("Cub") }
queue.async { print(sleepingBeears) }
//["Grandpa", "Cub"]

所以,我们在多线程操作集合的时候,要注意适当的时候添加锁来确保集合的正确性。

尽量使用不可变集合

优点:

  • 可读性更高
  • bug更少
  • 可以使用切片和lazy来进行可变集合的操作
  • 编译器性能更高

在我们能提前预估使用集合的大概长度的时候,我们可以通过以下的方式来创建固定容量的结合:

1
2
3
4
5
6
7
8
9
10
11
12
// Array reserveCapacity(_:)
var num = [Int]()
num.reserveCapacity(120)
for _ in 0...100 {
num.append(0)
}
//Set Set(minimumCapacity:)
var set = Set<Int>(minimumCapacity: 10)
//Dictionary Dictionary(minimumCapacity:)
var dict = Dictionary<String, String>(minimumCapacity: 10)

优点:如果你知道要添加到集合中多少个元素,使用此方法可以避免多次重新分配。

总结

  • 我们应该尽可能的使用first来访问集合的第一个元素
  • 切片是对原集合的强引用,不要长时间使用它,可能会造成内存泄漏
  • 我们在进行大量的链式运算而且只获取结果的部分数据时,我们可以使用 lazy functions
  • 要在改变集合之后在获取 index 来得到数据
  • 在多线程操作集合的时候,要注意适当的时候添加锁来确保集合的正确性

好了,到这里这篇文章就结束了。希望通过本篇文章让大家能在使用集合的时候更加得心应手,更加安全高效。Happy Day!

参考