Swift 进阶【一】内建集合类型

数组和可变性


在Swift中,数组是值类型。并且Swift中,Swift标准库中的所有集合类型都使用了“写时复制”这一技术,避免了大量副本导致性能下降。值类型的好处,不仅是性能有提高,其次就是能保证数组的不可变。

在Objective-C中,声明一个不可变的数组 NSArray 并不能保证数组的不可变。例如:

1
2
3
4
5
6
let mutableAry = NSMutableArray(array: [1, 2, 3])
/// 我们不想让 ary 被改变,声明为不可变数组
let ary: NSArray = mutableAry
/// 但是事实上它依然能够被 a 影响并改变
mutableAry.add(5)
ary // [1, 2, 3, 5]

正确的做法是,在赋值的时候,先进行拷贝:

1
2
3
4
5
let nsAry = mutableAry.copy() as! NSArray
mutableAry.add(10)

mutableAry // [1, 2, 3, 5, 10]
nsAry // [1, 2, 3, 5]

值类型的优点

* 不变性
值类型的变量是严格的被一个所有者控制的
* 独立性
引用类型是一种隐式的共享实例
* 安全性
在多线程时可以放心值传递

不可变性在安全中的作用

选择值类型而不是引用类型的一个主要原因是能让你的代码变得更加简单。你在任何情况下用一个值类型,都能够假设你的其他代码不会使它改变,这通常在多线程环境中很有用,如果一个线程中使用的数据被另一个线程给意外的修改了,这通常会产生非常严重的Bug,且相当难以调试。
由于只有当你需要修改数据时两者的区别才会得到体现,所以当你的实例不会对数据进行修改的时候,值类型和引用类型看起来是完全相同的。
你也许会想,写一个完全不可变的类,这或许是有价值的,使用Cocoa的NSObject能简化这个过程,并且能很好地保持原有的语义。现在,你能通过使用不可变的存储属性,以及避免暴露修改数据的接口,从而在Swift里实现一个不可变的类。事实上,大多数的Cocoa类,比如NSURL等,都被设计为不可变的类,然而,Swift当前并没有提供任何语言机制去强制申明一个类不可改变(比如子类化就能修改一个类的实现),只有结构体和枚举才是强制不可变的。

使用函数将行为参数化


类似于如下的代码:

1
2
3
4
5
6
7
let names = ["Paula", "Elena", "Zoe"]
var lastNameEndingInA: String?
for name in names.reversed() where name.hasSuffix("a") {
lastNameEndingInA = name
break
}
lastNameEndingInA // Optional("Elena")

可以将其改写成 Sequence 的一个扩展,详解,参考函数式编程

1
2
3
4
5
6
7
8
extension Sequence {
func last(where predicate: (Iterator.Element) -> Bool) -> Iterator.Element? {
for element in reversed() where predicate(element) {
return element
}
return nil
}
}

Filter函数


一个关于性能的小提示:如果你正在写下面这样的代码,请不要这么做!

1
bigArray.filter { someCondition }.count > 0

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

1
bigArray.contains { someCondition }

Reduce函数


使用reduce函数可以向下面一样:

1
let sum = fibs.reduce(0) { total, num in total + num } // 12

运算符也是函数,所以我们也可以把上面的例子写成这样:

1
fibs.reduce(0, +) // 12

集合代数


Set中,可以对Set执行数学中的基本集合操作补集交集并集
因为在标准库中,Set是唯一实现了 SetAlgebra 协议的类型。

但是这个协议在 Foundation 中还被另外两个很有意思的类型实现了:那就是 IndexSet 和 CharacterSet。

具体可以参考 SetAlgebra 协议。

补集

1
2
3
4
5
let iPods: Set = ["iPod touch", "iPod nano", "iPod mini", "iPod shuffle", "iPod Classic"]
let discontinuedIPods: Set = ["iPod mini", "iPod Classic"]
let currentIPods = iPods.subtracting(discontinuedIPods)

// ["iPod shuffle", "iPod nano", "iPod touch"]

交集

1
2
3
4
let touchscreen: Set = ["iPhone", "iPad", "iPod touch", "iPod nano"]
let iPodsWithTouch = iPods.intersection(touchscreen)

// ["iPod touch", "iPod nano"]

并集

1
2
3
4
var discontinued: Set = ["iBook", "Powerbook", "Power Mac"]
discontinued.formUnion(discontinuedIPods)

// ["iBook", "iPod mini", "Powerbook", "Power Mac", "iPod Classic"]

索引集合和字符集合


IndexSet 表示了一个由正整数组成的集合。当然,你可以用Set来做这件事,但是IndexSet更加高效,因为它内部使用了一组范围列表进行实现。

如果选择 Set<Int> 来存储1000以内所选择的数字的话,最坏的情况是存储1000个元素。如果用 IndexSet ,即使选择前500个数字的话,IndexSet 里其实只存储了选择的首位和末位两个整数值。

1
2
3
4
var indices = IndexSet()
indices.insert(integersIn: 1..<5)
indices.insert(integersIn: 11..<15)
let evenIndices = indices.filter { $0 % 2 == 0 } // [2, 4, 12, 14]

CharacterSet 将在字符串一章讨论。

思考:

过滤数组中重复的元素,并且保持数组中元素的顺序。

我们可以用 Set 去除重复,但是 Set 是无序,我们可以结合闭包来实现。这里我们写一个 Sequence 的扩展。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
extension Sequence where Iterator.Element: Hashable {
func unique() -> [Iterator.Element] {
var seen: Set<Iterator.Element> = []
return filter {
if seen.contains($0) {
return false
} else {
seen.insert($0)
return true
}
}
}
}
[1,2,3,12,1,3,4,5,6,4,6].unique() // [1, 2, 3, 12, 4, 5, 6]

2018年3月11日 上午3:35 更新


关于去除数组中重复项的更函数式的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// SwifterSwift: Remove all duplicate elements from Array.
///
/// [1, 2, 2, 3, 4, 5].removeDuplicates() -> [1, 2, 3, 4, 5]
/// ["h", "e", "l", "l", "o"]. removeDuplicates() -> ["h", "e", "l", "o"]
///
public mutating func removeDuplicates() {
// Thanks to https://github.com/sairamkotha for improving the method
self = reduce([]){ $0.contains($1) ? $0 : $0 + [$1] }
}

/// SwifterSwift: Return array with all duplicate elements removed.
///
/// [1, 2, 2, 3, 4, 5, 5].duplicatesRemoved() -> [ 2, 5]
/// ["h", "e", "l", "l", "o"]. duplicatesRemoved() -> ["l"]
///
/// - Returns: an array of unique elements.
public func duplicatesRemoved() -> [Element] {
// Thanks to https://github.com/sairamkotha for improving the property
return reduce([]){ ($0 as [Element]).contains($1) ? $0 : $0 + [$1] }
}