第10章 集合:Array、Dictionary 与 Set

第十章:集合:Array、Dictionary 与 Set

集合是程序的收纳间。数组排好队,字典按名字找,集合负责去重。它们都能装很多东西,但最重要的共同点其实不是“能装”:在 Swift 里,把集合赋给另一个变量,得到的是独立的副本,而不是指向同一堆数据的遥控器。

10.1 Array:有顺序的列表

数组用方括号书写,元素类型必须一致:

1
2
3
var fruits = ["apple", "banana", "cherry"]
print(fruits.count, fruits.first as Any, fruits.last as Any)
// prints: 3 Optional("apple") Optional("cherry")

first 和 last 返回可选值,因为空数组没有首尾元素:

1
2
3
let empty: [Int] = []
print(empty.first as Any)
// prints: nil

下标访问返回非可选值,越界会直接崩溃:

1
2
print(fruits[1])
// prints: banana

增删改:

1
2
3
4
5
6
7
8
9
fruits.append("date")
fruits.insert("avocado", at: 0)
print(fruits)
// prints: ["avocado", "apple", "banana", "cherry", "date"]

fruits[0] = "apricot"
fruits.remove(at: 1)
print(fruits)
// prints: ["apricot", "banana", "cherry", "date"]

removeAll() 清空,contains(_:) 判断存在,firstIndex(of:) 找到位置:

1
2
3
4
print(fruits.contains("banana"))
// prints: true
print(fruits.firstIndex(of: "cherry") as Any)
// prints: Optional(2)

数组的切片类型是 ArraySlice,它和 Substring 一样会保留原数组的存储:

1
2
3
let slice = fruits[1...2]
print(slice, type(of: slice))
// prints: ["banana", "cherry"] ArraySlice<String>

10.2 Dictionary:用键找值

字典存放“键—值”对,键必须遵循 Hashable:

1
2
3
var scores = ["Lin": 90, "Mia": 85]
print(scores["Lin"] as Any)
// prints: Optional(90)

下标返回可选值,因为键可能不存在:

1
2
print(scores["Nova"] as Any)
// prints: nil

更新和删除:

1
2
3
4
5
6
7
8
scores["Nova"] = 77
scores["Lin"] = 95
print(scores["Lin"] as Any)
// prints: Optional(95)

let removed = scores.removeValue(forKey: "Mia")
print(removed as Any)
// prints: Optional(85)

updateValue(_:forKey:) 会返回旧值,这也是可选值:

1
2
3
let old = scores.updateValue(100, forKey: "Nova")
print(old as Any, scores["Nova"] as Any)
// prints: Optional(77) Optional(100)

遍历:

1
2
3
4
5
for (name, score) in scores.sorted(by: { $0.key < $1.key }) {
    print("\(name): \(score)")
}
// prints: Lin: 95
// prints: Nova: 100

字典本身是无序的,所以想要稳定输出时通常先排序。

10.3 Set:只关心“有没有”

集合存放不重复的值,元素必须遵循 Hashable:

1
2
3
var tags: Set<String> = ["swift", "ios", "swift"]
print(tags.count)
// prints: 2

集合的典型操作是交、并、差:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let a: Set = [1, 2, 3, 4]
let b: Set = [3, 4, 5]

print(a.intersection(b).sorted())
// prints: [3, 4]
print(a.union(b).sorted())
// prints: [1, 2, 3, 4, 5]
print(a.subtracting(b).sorted())
// prints: [1, 2]
print(a.symmetricDifference(b).sorted())
// prints: [1, 2, 5]

判断包含关系:

1
2
3
4
print(a.isSuperset(of: [1, 2]))
// prints: true
print(a.isDisjoint(with: [9, 10]))
// prints: true

名字都取得很直白,对着结果读一遍就记住了:intersection 是交集,union 是并集,subtracting 是"从左边那份里剔掉右边也有的元素",symmetricDifference 是"只出现在其中一边"的对称差;isSuperset(of:) 问"我是不是包含它全部",isDisjoint(with:) 问"我们有没有交集"。集合运算背后是哈希表,所以这些操作通常比用数组手写循环快得多,也不会产生重复元素。

10.4 值语义:复制的是内容,不是地址

数组、字典和集合都是值类型:

1
2
3
4
5
6
7
8
var original = [1, 2, 3]
var copy = original
copy.append(4)

print(original)
// prints: [1, 2, 3]
print(copy)
// prints: [1, 2, 3, 4]

这里的“复制”不是立刻把所有元素抄一遍。Swift 使用写时复制(Copy-on-Write):只有某一方真正修改时,底层存储才会分离。第 27 章会拆开这套机制,现在先记住结论:每次赋值在语义上都是独立副本,性能通常在幕后优化。

字典和集合同理:

1
2
3
4
5
var d1 = ["a": 1]
var d2 = d1
d2["b"] = 2
print(d1.count, d2.count)
// prints: 1 2

这里没有直接打印两个字典,因为字典是无序的,打印出来的键顺序不保证稳定,不适合写进“预期输出”。要比内容,用 ==;要稳定地展示,先 sorted()。

10.5 遍历、筛选与变换

数组、字典、集合都能用 for-in 遍历:

1
2
3
4
5
for fruit in ["apple", "pear"] {
    print(fruit)
}
// prints: apple
// prints: pear

map 变换每个元素,filter 保留满足条件的元素,reduce 把元素合成一个结果:

1
2
3
4
5
6
7
let numbers = [1, 2, 3, 4, 5]
print(numbers.map { $0 * 2 })
// prints: [2, 4, 6, 8, 10]
print(numbers.filter { $0.isMultiple(of: 2) })
// prints: [2, 4]
print(numbers.reduce(0, +))
// prints: 15

reduce(0, +) 里的 + 是“运算符当函数传”的写法,第 15B.10 节会专门讲它以及其他函数值的简写。

compactMap 会丢掉变换产生的 nil:

1
2
3
let texts = ["1", "two", "3"]
print(texts.compactMap(Int.init))
// prints: [1, 3]

遍历数组时向其中增删元素会出问题,因为迭代器依赖稳定的结构。需要修改时先收集待处理项,循环结束后再统一处理:

1
2
3
4
5
6
7
8
var values = [1, 2, 3]
var shouldRemove: [Int] = []
for value in values where value.isMultiple(of: 2) {
    shouldRemove.append(value)
}
values.removeAll { shouldRemove.contains($0) }
print(values)
// prints: [1, 3]

10.6 本章小结

集合有序可重复查找方式适合场景
Array是是下标、遍历顺序列表
Dictionary否键唯一键按名字查数据
Set否否包含判断、集合运算去重与成员关系

10.7 本章易错点速查

易错点正确理解
用越界下标“试试看”越界会崩溃,不能靠它判断存在
把字典下标当成一定有值字典下标返回可选值
认为数组赋值后共享修改值语义下两者独立
把不可哈希的值塞进 Set元素必须遵循 Hashable
期待字典遍历顺序固定字典无序,需要顺序就排序
边遍历边修改数组先记录待处理项,循环结束后统一修改
忘记 compactMapmap 会产生 [Int?],成对过滤解包用 compactMap

10.8 下章预告

数据有了容器,接下来就要让程序做决定。第十一章从 if、for、while 讲起,把控制流的骨架搭好;第十二章再进入 Swift 的 switch——那不是普通的 switch,而是一套模式匹配系统。

最后修改 September 19, 2026: 更新 (3489033b1)