談談 Swift 中的 map 和 flatMap

njwc2879 8年前發布 | 8K 次閱讀 Swift Apple Swift開發

map 和 flatMap 是 Swift 中兩個常用的函數,它們體現了 Swift 中很多的特性。對于簡單的使用來說,它們的接口并不復雜,但它們內部的機制還是非常值得研究的,能夠幫助我們夠好的理解 Swift 語言。

map 簡介

首先,咱們說說 map 函數如何使用。

let numbers = [1,2,3,4]
let result = numbers.map { $0 + 2 }
print(result)  // [3,4,5,6]

map 方法接受一個閉包作為參數, 然后它會遍歷整個 numbers 數組,并對數組中每一個元素執行閉包中定義的操作。 相當于對數組中的所有元素做了一個映射。 比如咱們這個例子里面的閉包是講所有元素都加 2 。 這樣它產生的結果數據就是 [3,4,5,6]。

初步了解之后,我們來看一下 map 的定義:

func map
(@noescape transform: (Self.Generator.Element) throws -> T) rethrows -> [T]

咱們拋開一些和關鍵邏輯無關的修飾符 @noescape,throws 這些,在整理一下就是這樣:

func map
(transform: (Self.Generator.Element) -> T) rethrows -> [T]

map 函數接受一個閉包, 這個閉包的定義是這樣的:

(Self.Generator.Element) -> T

它接受 Self.Generator.Element 類型的參數, 這個類型代表數組中當前元素的類型。 而這個閉包的返回值,是可以和傳遞進來的值不同的。 比如我們可以這樣:

let stringResult = numbers.map { "No. \($0)" }
// ["No. 1", "No. 2", "No. 3", "No. 4"]

這次我們在閉包裝把傳遞進來的數字拼接到一個字符串中, 然后返回一個組數, 這個數組中包含的數據類型,就是我們拼接好的字符串。

這就是關于 map 的初步了解, 我們繼續來看 flatMap。

flatMap

map 可以對一個集合類型的所有元素做一個映射操作。 那么 flatMap 呢?

讓我們來看一個 flatMap 的例子:

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

我們對同樣的數組使用 flatMap 進行處理, 得到了同樣的結果。 那 flatMap 和 map 到底有什么區別呢?

咱們再來看另一個例子:

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

這里就看出差別了。 對于二維數組, map 和 flatMap 的結果就不同了。 我們先來看第一個調用:

var res = numbersCompound.map { $0.map{ $0 + 2 } }
// [[3, 4, 5], [6, 7, 8]]

numbersCompound.map { ... } 這個調用實際上是遍歷了這里兩個數組元素 [1,2,3] 和 [4,5,6]。 因為這兩個元素依然是數組,所以我們可以對他們再次調用 map 函數:$0.map{ $0 + 2 }。 這個內部的調用最終將數組中所有的元素加 2。

再來看看 flatMap 的調用:

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

flatMap 依然會遍歷數組的元素,并對這些元素執行閉包中定義的操作。 但唯一不同的是,它對最終的結果進行了所謂的 “降維” 操作。 本來原始數組是一個二維的, 但經過 flatMap 之后,它變成一維的了。

flatMap 是如何做到的呢,它的原理是什么,為什么會存在這樣一個函數呢? 相信此時你腦海中肯定會浮現出類似的問題。

下面咱們再來看一下 flatMap 的定義, 還是拋去 @noescape, rethrows 這些無關邏輯的關鍵字:

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

和 map 不同, flatMap 有兩個重載。 參照我們剛才的示例, 我們調用的其實是第二個重載:

func flatMa
p(transform: (Self.Generator.Element) -> S) -> [S.Generator.Element]

flatMap 的閉包接受的是數組的元素,但返回的是一個 SequenceType 類型,也就是另外一個數組。 這從我們剛才這個調用中不難看出:

numbersCompound.flatMap { $0.map{ $0 + 2 } }

我們傳入給 flatMap 一個閉包 $0.map{ $0 + 2 } , 這個閉包中,又對 $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 + 2 } }
// [3, 4, 5, 6, 7, 8]

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

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

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

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

仔細想想是不是這樣呢~

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 的結果集時,會得到這樣的輸出:
``` swift
["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 值的效果了。

關于 Optional 和 if let 語句可以參看: 淺談 Swift 中的 Optionals

結尾

關于 Swift 中的 map 和 flatMap, 看完這篇內容是不會會對你有所啟發呢。 當然, 關于這兩個函數我們這里并沒有完全討論完。 它們背后還有著更多的思想。 關于本篇文章的代碼,大家還可以來 Github 上面參看 https://github.com/swiftcafex/mapAndFlatmap

小練習

從這期開始,每篇內容會給大家出一兩個小小的練習, 大家可以在留言中直接回復你的答案,與大家一起交流, 讓大家的閱讀過程更加有趣。

將類型為 [Int] 的數組 [1,2,3,4] 中所有的元素乘以 2。

將類型為 [String?] 的數組 [“ab”, “cc” , nil, “dd”] 中的 nil 元素過濾掉。 分別用 map, filter 與 flatMap 的方式都實現一遍。

 

來自: http://www.cocoachina.com/swift/20160527/16467.html

 

 本文由用戶 njwc2879 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!