Swift flatMap詳解

Swift flatMap詳解

先看下flatMap的用法

Sequence.flatMap<S>(_: (Element) -> S) 
    -> [S.Element] where S : Sequence
Optional.flatMap<U>(_: (Wrapped) -> U?) -> U?
Sequence.flatMap<U>(_: (Element) -> U?) -> [U]

Map 可以對一個集合類型的所有元素做一個映射操作, 那麼 flatMap 跟Map有什麼區別呢?

讓我們來看一個 flatMap 的用法:

result = numbers.flatMap { $0 + 1 }
// [2,3,4,5]

同樣的數組使用 flatMap 進行處理,得到的結果是一樣的,那 flatMap 和 map 到底有什麼區別呢?
讓我們再看一個場景

let numbersCompound = [[1,2,3],[4,5,6]];
var res = numbersCompound.map { $0.map{ $0 + 1 } }
// [[2,3,4], [5,6,7]]
var flatRes = numbersCompound.flatMap { $0.map{ $0 + 1 } }
// [2,3,4,5,6,7]

這裏就看出差別了,對於二維數組來說,Map和flatMap的結果就不同了。
相比於Map,flatMap 依然會遍歷數組的元素,並對這些元素執行閉包中定義的操作。 但唯一不同的是,它對最終的結果進行了所謂的 “降維” 操作。 本來原始數組是一個二維的, 但經過 flatMap 之後,它變成一維的了。
下面咱們再來看一下 flatMap 的函數定義

func flatMap(transform: (Self.Generator.Element) throws -> T?) -> [T]
func flatMap(transform: (Self.Generator.Element) -> S) -> [S.Generator.Element]

flatMap 的其中一個重載

flatMap 有兩個重載。 參照我們剛纔的示例,我們調用的其實是第二個重載,flatMap 的閉包接受的是數組的元素,但返回的是一個 SequenceType 類型,也就是另外一個數組。 這從我們剛纔這個調用中不難看出我們傳入給 flatMap 一個閉包 $0.map{ $0 + 1 } , 這個閉包中,又對 $0 調用了 map 方法, 從 map 方法的定義中我們能夠知道,它返回的還是一個集合類型,也就是 SequenceType。 所以我們這個 flatMap 的調用對應的就是第二個重載形式。

那麼爲什麼 flatMap 調用後會對數組降維呢? 我們可以從它的源碼中窺探一二(Swift 不是開源了嗎~)。
文件位置: swift/stdlib/public/core/SequenceAlgorithms.swift.gyb

extension Sequence {
//...
public func flatMap(
@noescape transform: (${GElement}) throws -> S
) rethrows -> [S.${GElement}] {
var result: [S.${GElement}] = []
for element in self {
result.append(contentsOf: try transform(element))
}
return result
}
//...
}

這就是 flatMap 的完整源碼了, 它的源碼也很簡單, 對遍歷的每一個元素調用try transform(element)。transform 函數就是我們傳遞進來的閉包。
然後將閉包的返回值通過result.append(contentsOf:)函數添加到 result 數組中。
那我們再來看一下result.append(contentsOf:)都做了什麼, 它的文檔定義是這樣:
Append the elements of newElements to self.
簡單說就是將一個集合中的所有元素,添加到另一個集合。 還以我們剛纔這個二維數組爲例:

let numbersCompound = [[1,2,3],[4,5,6]];
var flatRes = numbersCompound.flatMap { $0.map{ $0 + 1 } }
// [2,3,4,5,6,7]

flatMap 首先會遍歷這個數組的兩個元素 [1,2,3] 和 [4,5,6], 因爲這兩個元素依然是數組, 所以我們可以對他們再進行 map 操作: $0.map{ $0 + 1 }。

這樣, 內部的 $0.map{ $0 + 1 } 調用返回值類型還是數組, 它會返回 [2,3,4] 和 [5,6,7]。

然後, flatMap 接收到內部閉包的這兩個返回結果, 進而調用 result.append(contentsOf:)將它們的數組中的內容添加到結果集中,而不是數組本身。

那麼我們最終的調用結果理所當然就應該是[2,3,4,5,6,7]了。

仔細想想是不是這樣呢~

flatMap 的另一個重載

我們剛纔分析了半天, 其實只分析到 flatMap 的一種重載情況, 那麼另外一種重載又是怎麼回事呢:

func flatMap(transform: (Self.Generator.Element) -> T?) -> [T]
從定義中我們看出, 它的閉包接收的是 Self.Generator.Element 類型, 返回的是一個 T? 。 我們都知道,在 Swift 中類型後面跟隨一個 ?, 代表的是 Optional 值。 也就是說這個重載中接收的閉包返回的是一個 Optional 值。 更進一步來說,就是閉包可以返回 nil。

我們來看一個例子:

let optionalArray: [String?] = [``"AA"``, nil, ``"BB"``, ``"CC"``]
var optionalResult = optionalArray.flatMap{ $0 }
// ["AA", "BB", "CC"]`

這樣竟然沒有報錯, 並且 flatMap 的返回結果中, 成功的將原數組中的 nil 值過濾掉了。 再仔細觀察,你會發現更多。 使用 flatMap 調用之後, 數組中的所有元素都被解包了, 如果同樣使用 print 函數輸出原始數組的話, 大概會得到這樣的結果:

[Optional(``"AA"``), nil, Optional(``"BB"``), Optional(``"CC"``)]

而使用 print 函數輸出 flatMap 的結果集時,會得到這樣的輸出:
["AA", "BB", "CC"]
也就是說原始數組的類型是 [String?] 而 flatMap 調用後變成了 [String]。 這也是 flatMap 和 map 的一個重大區別。 如果同樣的數組,我們使用 map 來調用, 得到的是這樣的輸出:
[Optional(``"AA"``), nil, Optional(``"BB"``), Optional(``"CC"``)]
這就和原始數組一樣了。 這兩者的區別就是這樣。 map 函數值對元素進行變換操作。 但不會對數組的結構造成影響。 而 flatMap 會影響數組的結構。再進一步分析之前,我們暫且這樣理解。

flatMap 的這種機制,而已幫助我們方便的對數據進行驗證,比如我們有一組圖片文件名, 我們可以使用 flatMap 將無效的圖片過濾掉:

var imageNames = [``"test.png"``, ``"aa.png"``, ``"icon.png"``]
imageNames.flatMap{ UIImage(named: $0) }

那麼 flatMap 是如何實現過濾掉 nil 值的呢? 我們還是來看一下源碼:

extension Sequence {
// ...
public func flatMap(
@noescape transform: (${GElement}) throws -> T?
) rethrows -> [T] {
var result: [T] = []
for element in self {
if let newElement = try transform(element) {
result.append(newElement)
}
}
return result
}
}

依然是遍歷所有元素,並應用try transform(element)閉包的調用, 但關鍵一點是,這裏面用到了 if let 語句, 對那些只有解包成功的元素,纔會添加到結果集中:

if let newElement = try transform(element) {
result.append(newElement)
}

這樣, 就實現了我們剛纔看到的自動去掉 nil 值的效果了。

相關文章:
Swift Map詳解.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章