談談 Swift 中的 map 和 flatMap

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<T>(@noescape transform: (Self.Generator.Element) throws -> T) rethrows -> [T] 

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


func map<T>(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<T>(transform: (Self.Generator.Element) throws -> T?) -> [T]
func flatMap<S : SequenceType>(transform: (Self.Generator.Element) -> S) -> [S.Generator.Element]

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

func flatMap<S : SequenceType>(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<S : Sequence>(
    @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<T>(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<T>(
    @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

小練習

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

  1. 將類型爲 [Int] 的數組 [1,2,3,4] 中所有的元素乘以 2。
  2. 將類型爲 [String?] 的數組 ["ab", "cc" , nil, "dd"] 中的 nil 元素過濾掉。 分別用 map, filter 與 flatMap 的方式都實現一遍。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章