细谈map,flatMap和compactMap

Talk About map, flatMap and compactMap

Posted by Quincy-QC on 2020-01-19

前言

map是Swift中常用的函数,同时它还有两个兄弟级函数flatMapcompactMap,下面就根据官方声明和源码来谈谈这个三个函数之间的联系和区别,加以深刻记忆。

map

首先是map函数的使用:

1
2
3
let nums = [1, 2, 3]
let res = nums.map { $0 + 1 }
print(res) // [2, 3, 4]

map函数接受一个闭包函数作为参数,他会遍历整个序列元素,并对每个元素执行闭包函数中定义的操作。

然后我们再看下map函数的官方声明:

1
func map<T>(_ transform: (Value) throws -> T) rethrows -> [T]

我们来稍微解释下这个声明的意思,所需参数transform的类型很明显是一个闭包类型:将(Value)(序列元素的类型)转换为类型T,最终返回一个T类型的序列。

这就是map的初步了解,我们再看下一个。

flatMap

flatMap的声明和使用有两种,让我们分别来讲解一下。

先看第一种使用方法:

1
2
3
4
5
let nums = [[1, 2], [3, 4]]
let mapRes = nums.map { $0.map { $0 + 1 } }
let flatRes = nums.flatMap { $0.map { $0 + 1 } }
print(mapRes) // [[2, 3], [4, 5]]
print(flatRes) // [2, 3, 4, 5]

这样我们就看出mapflatMap的区别了。

我们看下map函数的官方声明:

1
func flatMap<SegmentOfResult>(_ transform: (Value) throws -> SegmentOfResult) rethrows -> [SegmentOfResult.Element] where SegmentOfResult : Sequence

这个声明比map相比就长了很多,但我们可以将它转化一下:

1
func flatMap<T>(_ transform: (Value) throws -> T) rethrows -> [T.Element] where T : Sequence

这么看其实就已经和map的声明差不多了,同时不同的地方表现在以下几个方面:T的类型有了约束,必须是继承Sequence协议,因为它的返回值也变成了[T.Element],这个地方的关键点就是造成与map函数不同的地方。

然后我们看下flatMap源码部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
extension LazySequenceProtocol {
/// Returns the concatenated results of mapping the given transformation over
/// this sequence.
///
/// Use this method to receive a single-level sequence when your
/// transformation produces a sequence or collection for each element.
/// Calling `flatMap(_:)` on a sequence `s` is equivalent to calling
/// `s.map(transform).joined()`.
///
/// - Complexity: O(1)
@inlinable // lazy-performance
public func flatMap<SegmentOfResult>(
_ transform: @escaping (Elements.Element) -> SegmentOfResult
) -> LazySequence<
FlattenSequence<LazyMapSequence<Elements, SegmentOfResult>>> {
return self.map(transform).joined()
}
}

我们通过源码可以看出,flatMap函数本身也是调用了map函数,同时进行了joined操作,joined操作可以将序列包裹序列的元素重新组合成一个序列(只能拆第二层的序列,第三层的依旧是原类型),所以这就是flatMap函数的不同点。

flatMap函数的另一种使用方式其实已经弃用,功能已经划分给compactMap函数,但我们可以看下它的声明:

1
2
@available(swift, deprecated: 4.1, renamed: "compactMap(_:)", message: "Please use compactMap(_:) for the case where closure returns an optional value")
func flatMap<ElementOfResult>(_ transform: (Value) throws -> ElementOfResult?) rethrows -> [ElementOfResult] // Deprecated

下面来看下compactMap函数。

compactMap

使用方法:

1
2
3
let nums = [1, 2, nil, 3, 4]
let res = nums.compactMap { $0 }
print(res) // [1, 2, 3, 4]

如果使用map函数实现上面代码,会直接报错无法运行,但是compactMap函数不会报错,还会把nil剔除,同时可以将可选类型解包。这在实际场景中是个非常有用的机制,可以用条件闭包将非法或无效的内容过滤。

然后我们先来看下compactMap的官方声明:

1
func compactMap<ElementOfResult>(_ transform: (Value) throws -> ElementOfResult?) rethrows -> [ElementOfResult]

将它转化一下:

1
func compactMap<T>(_ transform: (Value) throws -> T?) rethrows -> [T]

map函数不同地方就在于transform闭包参数的返回值是T的可选类型,最终的返回结果是解包后的[T]

我们在根据源码分析下它是如何筛选并解包的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
extension LazySequenceProtocol {
/// Returns the non-`nil` results of mapping the given transformation over
/// this sequence.
///
/// Use this method to receive a sequence of non-optional values when your
/// transformation produces an optional value.
///
/// - Parameter transform: A closure that accepts an element of this sequence
/// as its argument and returns an optional value.
///
/// - Complexity: O(1)
@inlinable // lazy-performance
public func compactMap<ElementOfResult>(
_ transform: @escaping (Elements.Element) -> ElementOfResult?
) -> LazyMapSequence<
LazyFilterSequence<
LazyMapSequence<Elements, ElementOfResult?>>,
ElementOfResult
> {
return self.map(transform).filter { $0 != nil }.map { $0! }
}
}

非常容易理解,同样是调用了map函数,并调用filter函数进行非空筛选,最终进行解包。这也就不难理解compactMap函数的实现结果了。

结语

将这个三个函数剖析之后更加利于我们对于函数的理解,在实际场景中也能更好地根据对应情况使用不同的函数。