Swift Set 与高阶函数
Swift 的 Set 用来保存一组无序且不重复的值,适合做去重和集合运算。
高阶函数指的是可以接收函数作为参数、或者把函数作为返回值的函数,标准库里的 map、filter、reduce 都属于这一类。
本文先讲 Set 的创建与集合运算,再讲集合类型上最常用的几个高阶函数,最后演示把它们串起来的链式写法。
创建 Set
Set 的元素类型必须遵循 Hashable 协议,Swift 的 Int、String、Double、Bool 等基本类型都满足这个要求。
用数组字面量可以直接创建 Set,重复的元素会被自动去掉。
实例
var runoobSet: Set<String> = ["Swift", "Kotlin", "Rust"]
runoobSet.insert("Go") // 插入一个元素
print(runoobSet.count) // count 是元素个数
print(runoobSet.contains("Swift")) // 判断是否包含某个元素
print(runoobSet.contains("Python"))
print(runoobSet.sorted()) // Set 无序,排序后再输出才稳定
// 数组里的重复元素会自动去重
let repeated: Set<Int> = [1, 2, 2, 3, 3, 3]
print(repeated.sorted())
4 true false ["Go", "Kotlin", "Rust", "Swift"] [1, 2, 3]
需要注意,Set 是无序集合,直接打印一个 Set 时元素的顺序并不固定,不同运行环境可能不一样。
想让输出稳定,先调用 sorted() 排成数组再打印,本文所有 Set 的输出都采用这种写法。
注意:Set 要求元素可哈希,所以元素类型必须遵循 Hashable 协议。只包含 Hashable 成员的结构体,编译器可以自动合成 Hashable 实现,不需要手写。
增删元素与可变集合运算
Set 提供了 insert、remove 等方法增删元素,还提供了以 form 开头的原地运算方法,直接在原集合上修改而不产生新集合。
实例
var s: Set<Int> = [1, 2, 3]
s.insert(4) // 插入 4
s.remove(1) // 删除 1
s.formUnion([5, 6]) // 并集运算并写回 s
s.formIntersection([2, 3, 5]) // 交集运算并写回 s
s.subtract([3]) // 差集运算并写回 s
print(s.sorted())
// remove 删除成功时返回被删掉的元素,元素不存在时返回 nil
var t: Set<String> = ["RUNOOB", "runoob"]
print(t.remove("runoob") ?? "不存在")
print(t.remove("www.runoob.com") ?? "不存在")
[2, 5] runoob 不存在
Set 与 Array 之间可以自由转换:Set(array) 得到去重后的集合,Array(set) 把集合变回数组。
集合运算:并集、交集、差集与对称差
这几个方法都返回一个全新的 Set,不会修改原来的集合,下表的 a 和 b 分别代表两个集合。
| 方法 | 运算 | 结果说明 |
|---|---|---|
a.union(b) | 并集 | a 和 b 中出现过的所有元素 |
a.intersection(b) | 交集 | 同时在 a 和 b 中的元素 |
a.subtracting(b) | 差集 | 在 a 中但不在 b 中的元素 |
a.symmetricDifference(b) | 对称差 | 只在其中一个集合里出现的元素 |
实例
let b: Set<Int> = [3, 4, 5, 6]
print("并集:", a.union(b).sorted()) // 两边所有元素
print("交集:", a.intersection(b).sorted()) // 两边共有元素
print("差集:", a.subtracting(b).sorted()) // 只在 a 中
print("对称差:", a.symmetricDifference(b).sorted()) // 只在其中一边
并集: [1, 2, 3, 4, 5, 6] 交集: [3, 4] 差集: [1, 2] 对称差: [1, 2, 5, 6]
对称差可以这样理解:先求并集,再减去交集,剩下的就是只属于其中一边的元素。
子集、超集与不相交判断
这几个方法返回 Bool,用来判断两个集合之间的包含关系。
实例
let big: Set<String> = ["Swift", "Kotlin", "Rust"]
print(small.isSubset(of: big)) // true,small 的元素都在 big 里
print(big.isSuperset(of: small)) // true,big 包含 small 的全部元素
print(small.isStrictSubset(of: big)) // true,真子集:两个集合不相等
print(big.isDisjoint(with: ["Go"])) // true,没有公共元素
// 两个集合完全相等时
let same: Set<String> = ["Swift", "Kotlin", "Rust"]
print(big.isSubset(of: same)) // true,相等也算子集
print(big.isStrictSubset(of: same)) // false,真子集要求不相等
true true true true true false
isSubset 与 isStrictSubset 的区别只在两个集合相等时体现:相等时前者为 true,后者为 false。
注意:空集合是任何集合的子集,也是它自己的子集。判断「是否没有公共元素」要用 isDisjoint(with:),不要用交集是否为空来绕一圈。
高阶函数概览
高阶函数的共同特点是接收一个闭包作为参数,用来描述「对每个元素做什么」。
下面这张表列出本文要讲的八个函数,它们都定义在 Sequence 或 Collection 协议上,所以数组、Set、字典都能用。
| 函数 | 作用 | 返回值 |
|---|---|---|
map | 把每个元素按规则转换成另一个值 | 等长的新数组 |
filter | 保留满足条件的元素 | 新数组 |
reduce | 把所有元素合并成一个值 | 单个结果 |
compactMap | 转换并丢弃结果为 nil 的元素 | 新数组 |
flatMap | 把嵌套结构展平一层 | 新数组 |
forEach | 对每个元素执行操作,不使用返回值 | Void |
sorted | 按规则排序 | 新数组 |
contains(where:) | 判断是否存在满足条件的元素 | Bool |
map 与 filter
map 把每个元素「变成」另一个值,返回的数组长度和原集合一致。
filter 则像一道筛子,只留下闭包返回 true 的元素。
实例
// map:对每个元素应用闭包,返回等长的新数组
print(numbers.map { $0 * 2 })
let names = ["runoob", "swift"]
print(names.map { $0.uppercased() })
// filter:保留闭包返回 true 的元素
print(numbers.filter { $0 % 2 == 0 })
// map 作用在 Set 上时,返回值是数组而不是 Set
let langs: Set<String> = ["Swift", "Go", "Rust"]
print(langs.map { $0.count }.sorted())
[2, 4, 6, 8, 10] ["RUNOOB", "SWIFT"] [2, 4] [2, 4, 5]
注意 $0 是闭包简写参数,代表当前正在处理的元素。
reduce 合并元素
reduce 把一个集合「折叠」成一个值,第一个参数是初始值,闭包接收累计值和当前元素两个参数。
实例
// 完整写法:$0 是累计值,$1 是当前元素
print(numbers.reduce(0) { $0 + $1 })
// 运算符函数写法,效果完全相同
print(numbers.reduce(0, +))
// 求最大值:初始值取第一个元素
print(numbers.reduce(numbers[0]) { $0 > $1 ? $0 : $1 })
15 15 5
reduce 的初始值决定了结果类型,例如初始值给空字符串就能把字符串数组拼起来。
compactMap 与 flatMap
compactMap 会在转换的同时丢掉 nil,flatMap 则负责把嵌套的集合拍平成一层。
实例
let raw = ["1", "2", "runoob", "4"]
print(raw.compactMap { Int($0) })
// flatMap:把嵌套数组拍平
let nested = [[1, 2], [3, 4], [5]]
print(nested.flatMap { $0 })
// flatMap 也常用于把多个字符串按空格拆开后合并成一个数组
let sentences = ["Hello Runoob", "Welcome to RUNOOB"]
print(sentences.flatMap { $0.split(separator: " ") })
[1, 2, 4] [1, 2, 3, 4, 5] ["Hello", "Runoob", "Welcome", "to", "RUNOOB"]
如果直接用 map 做字符串转整数,结果会是一个 [Int?],里面夹杂着 nil,compactMap 正好解决了这个问题。
注意:闭包返回可选值时用 compactMap。flatMap 处理的是「集合的集合」,而 compactMap 处理的是「可选值的集合」,两者不要混用。
forEach、sorted 与 contains(where:)
forEach 用于遍历并执行副作用,sorted 返回排好序的新数组,contains(where:) 用来做条件查找。
实例
// forEach:只执行操作,没有返回值
numbers.forEach { print("元素: \($0)") }
let names = ["runoob", "swift"]
print(names.sorted()) // 默认升序
print(numbers.sorted(by: >)) // 传入比较闭包,实现降序
// 按元组的第二个元素排序,再取回第一个元素
let sites = [("Runoob", 2014), ("Google", 1998), ("Apple", 1976)]
print(sites.sorted { $0.1 < $1.1 }.map { $0.0 })
// contains(where:):是否存在满足条件的元素
print(numbers.contains(where: { $0 > 4 }))
print(numbers.contains(where: { $0 > 100 }))
元素: 1 元素: 2 元素: 3 元素: 4 元素: 5 ["runoob", "swift"] [5, 4, 3, 2, 1] ["Apple", "Google", "Runoob"] true false
sorted 总是返回一个排好序的新数组,不会修改原来的集合,原顺序需要保留时直接用返回值即可。
链式调用
这些高阶函数都返回数组或单个值,可以一个接一个地串起来,每一步只关心上一步的输出。
实例
let result = (1...10)
.filter { $0 % 2 == 0 } // 2 4 6 8 10
.map { $0 * $0 } // 4 16 36 64 100
.reduce(0, +) // 累加
print(result)
// 用 Set 去重,再统一转成大写、排序、拼接成字符串
let words = ["Swift", "Kotlin", "Swift", "Rust"]
let summary = Set(words)
.map { $0.uppercased() }
.sorted()
.joined(separator: ", ")
print(summary)
220 KOTLIN, RUST, SWIFT
链式写法的可读性来自「每行只做一件事」,如果一行里塞进三个闭包,反而不如拆成几个变量。
常见问题
下面是学习 Set 与高阶函数时最常被问到的几个问题。
Set 和 Array 应该选哪个
需要去重、需要判断包含关系或做集合运算时用 Set;需要保持顺序、允许重复元素时用 Array。
Set 的查找是常数时间复杂度,元素数量很大且频繁判断「是否存在」时,Set 比 Array 快得多。
map 和 forEach 有什么区别
map 有返回值,它把每个元素转换后收集成新数组;forEach 没有返回值,只用于执行带副作用的操作,比如打印、写文件。
如果结果不需要保留,用 forEach 语义更清晰;如果需要新数组,就必须用 map。
为什么打印 Set 的顺序每次都不同
Set 内部用哈希表存储,元素顺序由哈希值和容量共同决定,官方不保证遍历顺序。
需要稳定输出时先 sorted(),或者在真正关心顺序的场合改用 Array。
自定义类型放进 Set 要做什么
让类型遵循 Hashable 协议即可。如果它的所有存储属性都是 Hashable 的,编译器会自动合成实现。
一旦自己手写了 == 或 hash(into:),就要保证相等的两个实例哈希值也相同,否则 Set 会出现重复元素。
