Swift中的集合类型高阶函数API
本篇是研读Chris Eidhof, Ole Begemann, Airspeed Velocity著的《Swift 进阶》(对应Swift 5.6)所做下的笔记,可能掺杂了些许本人浅薄的思考,如有纰漏,恳请赐教。
tip: 阅读本篇之前需要先了解swift中泛型与高阶函数
集合是值类型
值类型的特点之前的笔记里就有提到,因此它们在赋值过程中拷贝的是值而非引用,同样的,如果集合被定义为常量,不仅无法修改集合本身,也无法修改集合内的值。
数组(Array)
高阶函数API
试想一下要通过遍历一个数组得到一个新的数组我们可以这样做
let a = [1, 2, 3, 4, 5]
var b: [Int] = []
for i in a {
b.append(i + 1)
}
print(b) // [2, 3, 4, 5, 6]
Swift为这种操作提供了更为便利的API
map
Map旨在遍历数组中的每个值进行操作,传入一个闭包,闭包内部返回一个值,Map会遍历数组并用闭包的返回值创建一个新的数组返回,用例如下:
let a = [1, 2, 3, 4, 5]
let b = a.map({$0 + 1})
print(b) // [2, 3, 4, 5, 6]
优点:
- 更简洁,省去了模板代码,可读性更强。
- 不必新建变量append,可以直接通过常量接收值
它的具体实现不算复杂:
extension Array {
func map<T>(_ transform: (Element) -> T) -> [T] {
var result: [T] = []
result.reserveCapacity(count)
for x in self {
result.append(transform(x))
}
return result
}
}
诸如此类的高阶函数API还有很多,例如
filter
filter会传入一个返回Bool类型的闭包表达式,它会遍历数组并根据闭包返回的Bool值决定要保留的元素,并将它们组成一个新的数组,用例如下:
let a = [1, 2, 3, 4, 5]
let b = a.filter{$0 % 2 == 0}
print(b) // [2, 4]
filter的实现如下:
extension Array {
func filter(_ isIncluded: (Element) -> Bool) -> [Element] {
var result: [Element] = []
for x in self where isIncluded(x) {
result.append(x)
}
return result
}
}
reduce
reduce的用法稍微复杂一点,但也不算很难理解,先来看它的声明
@inlinable public func reduce<Result>(_ initialResult: Result, _ nextPartialResult: (Result, Element) throws -> Result) rethrows -> Result
可见它传入了两个参数,第一个是泛型值,代表“初始值“,我们称作A,第二个则是一个闭包,它也有两个参数,第一个是和之前相同的泛型值,我们称作B,第二个则是与数组元素类型相同的泛型值,我们称作C
reduce会遍历数组并执行闭包中的逻辑,B是以A为初始值并在闭包中迭代执行得到的值,同时也是reduce最终返回的值,C即是数组中的每个元素,大致了解了它的作用,我们结合具体用例看看:
let a = [1, 2, 3, 4, 5]
let b = a.reduce(0) {$0 + $1}
print(b) // 15
let c = a.reduce([0]) {$0 + [$1]}
print(c) // [0, 1, 2, 3, 4, 5]
可以看出reduce是一个用法比较灵活的API,如果你愿意,也可以通过reduce实现map和filter的功能
extension Array {
func map2<T>(_ transform: (Element) -> T) -> [T] {
return reduce([]) {
$0 + [transform($1)]
}
}
func filter2(_ isIncluded: (Element) -> Bool) -> [Element] {
return reduce([]) {
isIncluded($1) ? $0 + [$1] : $0
}
}
}
但这么做会由于使用了大量的中间数组(每次合并都会产生一个新的数组)导致空间复杂度变高(O(n²))
下面介绍reduce的另一个可以解决这个问题的版本
reduce(into:_:)
@inlinable public func reduce<Result>(into initialResult: Result, _ updateAccumulatingResult: (inout Result, Element) throws -> ()) rethrows -> Result
从参数类型上看是只有Result在作为闭包参数时变为了inout类型,相应的闭包也不需要再返回Result,这也意味着在遍历数组的过程中不会产生多个Result中间值,而是会将Result值覆盖到初始值上,如此实现的filter复杂度回到了O(n)
extension Array {
func filter3(_ isIncluded: (Element) -> Bool) -> [Element] {
return reduce(into: []) { result, element in
if isIncluded(element) {
result.append(element)
}
}
}
}
joined()
这不是一个高阶函数API,但先介绍它有助于我们了解下一个API。 简单讲joined()可以使数组降维成一个新的数组(或字符串)
let a = [[[1, 2],[1]], [[3]], [[4]], [[5],[6]]]
let b = Array(a.joined()) //[[1, 2], [1], [3], [4], [5], [6]]
flatMap
那么flatMap简单讲就是将map之后得到的数组再joined()一次,上面的用例等价于
var a = [[[1, 2],[1]], [[3]], [[4]], [[5],[6]]]
let b = a.flatMap { $0 } //[[1, 2], [1], [3], [4], [5], [6]]
典型使用场景
合并两个数组的全部元素组合
let suits = ["♠", "♥", "♣", "♦"]
let ranks = ["J","Q","K","A"]
let result = suits.flatMap { suit in
ranks.map {
rank in (suit, rank)
}
}
/*
[("♠", "J"), ("♠", "Q"), ("♠", "K"), ("♠", "A"), ("♥", "J"), ("♥",
"Q"), ("♥", "K"), ("♥", "A"), ("♣", "J"), ("♣", "Q"), ("♣", "K"), ("♣", "A"), ("♦", "J"), ("♦", "Q"), ("♦", "K"), ("♦", "A")]
*/
//讲道理,挺绕的,这个在iOS开发应用的频率高吗,为什么面试都喜欢考
//等我用上了就回头把这句删了
forEach
基本上就是for循环,所以直接看代码
for element in [1,2,3] {
print(element)
}
[1,2,3].forEach { element in
print(element)
}
唯一的区别是在for循环中使用return会使你退出外部函数,而在forEach中使用return则类似于for循环中的continue,因为它只是从闭包内返回,并不会终止循环,更不会退出外部函数。
除此之外还有很多高阶函数可以自行了解:
- allSatisfy—针对一个条件测试所有元素。
- sort(by:),sorted(by:),lexicographicallyPrecedes(_:by:),和partition(by:)—重排元 素。
- firstIndex(where:),lastIndex(where:),first(where:),last(where:),和 contains(where:) — 一个元素是否存在?
- min(by:)和max(by:)—找到所有元素中的最小或最大值。
- elementsEqual(_:by:)和starts(with:by:)—将元素与另一个数组进行比较。
- split(whereSeparator:)—把所有元素分成多个数组。
- prefix(while:)—从头取元素直到条件不成立。
- drop(while:)—当条件为真时,丢弃元素;一旦不为真,返回其余的元素(和prefix类 似,不过返回相反的集合)。
- removeAll(where:)—删除所有符合条件的元素。