现在位置: 首页 > Swift 教程 > 正文

Swift Set 与高阶函数

Swift 的 Set 用来保存一组无序且不重复的值,适合做去重和集合运算。

高阶函数指的是可以接收函数作为参数、或者把函数作为返回值的函数,标准库里的 map、filter、reduce 都属于这一类。

本文先讲 Set 的创建与集合运算,再讲集合类型上最常用的几个高阶函数,最后演示把它们串起来的链式写法。


创建 Set

Set 的元素类型必须遵循 Hashable 协议,Swift 的 Int、String、Double、Bool 等基本类型都满足这个要求。

用数组字面量可以直接创建 Set,重复的元素会被自动去掉。

实例

// 用数组字面量创建 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 开头的原地运算方法,直接在原集合上修改而不产生新集合。

实例

// 以 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 a: Set<Int> = [1, 2, 3, 4]
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 small: Set<String> = ["Swift"]
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:),不要用交集是否为空来绕一圈。


高阶函数概览

高阶函数的共同特点是接收一个闭包作为参数,用来描述「对每个元素做什么」。

高阶函数流水线:数组依次经过 map、filter、reduce,每一步下方标注该步的输出集合

下面这张表列出本文要讲的八个函数,它们都定义在 Sequence 或 Collection 协议上,所以数组、Set、字典都能用。

函数作用返回值
map把每个元素按规则转换成另一个值等长的新数组
filter保留满足条件的元素新数组
reduce把所有元素合并成一个值单个结果
compactMap转换并丢弃结果为 nil 的元素新数组
flatMap把嵌套结构展平一层新数组
forEach对每个元素执行操作,不使用返回值Void
sorted按规则排序新数组
contains(where:)判断是否存在满足条件的元素Bool

map 与 filter

map 把每个元素「变成」另一个值,返回的数组长度和原集合一致。

filter 则像一道筛子,只留下闭包返回 true 的元素。

实例

let numbers = [1, 2, 3, 4, 5]

// 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 把一个集合「折叠」成一个值,第一个参数是初始值,闭包接收累计值和当前元素两个参数。

实例

let numbers = [1, 2, 3, 4, 5]

// 完整写法:$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 则负责把嵌套的集合拍平成一层。

实例

// compactMap:把字符串转成 Int,转不了的返回 nil 并被丢弃
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:) 用来做条件查找。

实例

let numbers = [1, 2, 3, 4, 5]

// 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 会出现重复元素。