Swift Tips 019 - Chaining optionals with map() and flatMap()

419 阅读3分钟

代码截图

代码出处: Swift Tips 019 by John Sundell

小笔记

这段代码在说什么

截图里 BEFORE 和 AFTER 在代码逻辑上完全一致,只是使用了两种不同的编码风格。前一种使用了常见的可选绑定,方法调用等手段,而后一种仅仅通过使用高阶函数就完成了所有的功能。

Sequence 里的 map, flatMap 和 compactMap

在开始话题之前,我们不妨先看看这三个函数在 Sequence 里的定义,

public func map<T>(_ transform: (Element) -> T) -> [T]

public func flatMap<SegmentOfResult>(_ transform: (Element) -> SegmentOfResult) -> [SegmentOfResult.Element] where SegmentOfResult : Sequence

public func compactMap<ElementOfResult>(_ transform: (Element) -> ElementOfResult?) -> [ElementOfResult]

乍一看,它们都是接受一个名为 transform 闭包作为参数并且整个方法的返回值是一个数组。但仔细一看,这两个关键点在细节上又有着细微的不同。

map

map 对 Sequence 元素进行某种规则的转换,例如:

let arr = [1, 2, 4]
// arr = [1, 2, 4]
let stringArr = arr.map {
    "No." + String($0)
}
// stringArr = ["No.1", "No.2", "No.4"]

flatMap

flatmap 里第一个函数闭包的定义是 (Element) -> SegmentOfResult,并且这里 SegmentOfResult 被定义成 SegmentOfResult : Sequence,所以它是接受一个数组元素,然后输出一个 SequenceType 类型的元素的闭包。有趣的是,flatMap 最终执行的结果并不是 SequenceType 数组,而是 Sequence 内部元素组成的数组,即 SegmentOfResult.Element,可能文字读起来有点绕,我们来一段代码:

let arr = [[1, 2, 3], [6, 5, 4]]
let flatArr = arr.flatMap {
    $0
}
// flatArr = [1, 2, 3, 6, 5, 4]

在这个例子中,数组 arr 调用 flatMap 时,元素 [1, 2, 3][6, 5, 4] 分别被传入闭包中,又直接被作为结果返回。但是,最终的结果中,却是由这两个数组中的元素共同组成的新数组:[1, 2, 3, 6, 5, 4]

compactMap

如果在 Sequence 里仔细查看的话,我们还可以看到一个已经标注为废弃的同名 flatMap 的 API,它的替代者就是我们马上要介绍的 compactMap

Swift 4.1 之前存在 2 个两个 flatMap 函数,虽然它们都是用来降维的,但其中一个除了 flat 之外其实还有 filter 的作用,在使用时容易产生歧义,所以社区认为最好把第二个版本重新拆分出来,使用一个新的方法命名,就产生了这个提案 SE-0187。 最初这个提案用了 filterMap 这个名字,但后来经过讨论,就决定参考了 Ruby 的 Array::compact 方法,使用 compactMap 这个名字

public func flatMap<ElementOfResult>(_ transform: (Element) -> ElementOfResult?) -> [ElementOfResult]

在这个函数里,闭包的定义变成了:(Element) -> ElementOfResult?,返回值 ElementOfResult 不像 flatMap 那样要求是一个数组了,而变成了一个 Optional 的任意类型。而 compactMap 最终输出的数组结果,其实不是这个 ElementOfResult? 类型,而是这个 ElementOfResult? 类型解包之后,不为 .None 的元数数组:[ElementOfResult]

用代码来总结一下它的功能:

let arr: [Int?] = [1, 2, nil, 4, nil, 5]
let intArr = arr.flatMap { $0 }
// intArr = [1, 2, 4, 5]

Optional 里的 map, flatMap

除了在 Sequence 协议下里使用 mapflatMap,在 Optional 里我们也能见到 mapflatMap 的身影。

public enum Optional<Wrapped> : _Reflectable, NilLiteralConvertible {
    case None
    case Some(Wrapped)
    
    public func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U?
    public func flatMap<U>(_ transform: (Wrapped) throws -> U?) rethrows -> U?
}

所以,对于一个 Optional 的变量来说,map 方法允许它再次修改自己的值,并且不必关心自己是否为 .None。例如:

let a1: Int? = 3
let b1 = a1.map{ $0 * 2 }
// b1 = 6

let a2: Int? = nil
let b2 = a2.map{ $0 * 2 }
// b2 = nil

相比于 map 而言,flatMap 能够处理闭包参数可能返回 nil 的情况。 例如:

let s: String? = "abc"
let v = s.flatMap { (a: String) -> Int? in
    return Int(a)
}

怎么选择

  • 在 Sequence 类型中,存在 map,flatMap 和 compact 三种转换方法
    • map 可以将 Sequence 里的元素进行一次类型转换
    • flatMap 等价于先 map 再 flatten(即数组降维)
    • compact 用于去掉结果中的 nil
  • 在 Optional 类型里,存在 map 和 flatMap
    • 当我们的输入是一个 Optional,同时我们需要在逻辑中处理这个 Optional 是否为 nil 时,就适合用 map 来替代原来的写法,使得代码更加简短。
    • 当我们的闭包参数有可能返回 nil 的时候,就可以使用 Optional 的 flatMap 方法。