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:)—删除所有符合条件的元素。

种一棵树最好的时间是在十年前,而后是现在。

Loading Disqus comments...
Table of Contents