Swift — Transforming an Array

讓我們對你的陣列進行一些轉換吧!

Jeremy Xue
Jeremy Xue ‘s Blog

--

✎ 前言:

常常我們在面對一連串的資料(也就是陣列)時,我們總是會對他進行一些操作,無論你是使用 for-inforEachfiltermap 等等…。我們不外乎都是想要透過這些方法來將資料轉換改變成我們想要的結果。

而這篇文章主要會介紹的是有關於「轉換陣列」的相關內容,這幾種方式往往透過映射(mapping)的方式將原始陣列,轉換成你所想要的結果,而大致上會圍繞在以下幾個函數和屬性中:

  • map
  • flatMap
  • compactMap
  • reduce
  • lazy

✏︎ 轉換陣列:

⒈ map(_:)

通常我們會使用到 map 函數的原因,通常是以下幾種:

  1. 某個類型的陣列轉換成另外一個類型的陣列
  2. 透過 map 返回一個將原始元素進行轉換後的陣列

首先我們先來實作上述第 1 點的內容:

首先 lowercaseNamescast 映射後的返回結果,而 lowercaseNames 中所有元素都是 cast 中每個字串經過 lowercased 後的結果。而 letterCounts 也是 cast 映射後的返回結果,letterCounts 中所有元素都是 cast 中每個字串 count 的結果。

而第 2 點其實也是差不多的操作,我們也可以單純對原始陣列中的每個元素進行運算改變來返回一個結果:

所以經由這個範例你應該可以知道當一個陣列進行 map 時,其實就是將陣列中的每個元素轉變為另一個新的元素,並返回整個陣列,因此才稱為映射

⒉ flatMap(_:)

如果你剛剛了解了什麼是 map,那麼這個 flatMap 其實就是 flat + map 的結果。而 flatMap 最常是用來處理攤平某個陣列的狀況,首先我們來看一下以下範例:

首先我們讓 numbers 經過 map 後,產生了一個 [Array<Int>] 類型的 mapped ,而簡單來說就是根據 numbers 中元素的數字產生相對應數量的 [Int] 陣列。而這時候如果你希望把 [[Int]] 變成 [Int] ,也就是把 [[1], [2, 2], [3, 3, 3], [4, 4, 4, 4]] 轉換成 [1, 2, 2, 3, 3, 3, 4, 4, 4, 4] 的話,那麼使用 flatMap 來做非常合適,因為這就是一個所謂攤平的操作。

因此你可以看到我們對 number 使用 flatMapmap 閉包,但是經由 flatMap 的輸出結果會是 [Int] 的陣列 [1, 2, 2, 3, 3, 3, 4, 4, 4, 4] 而不是 [[Int]] 的陣列 [[1], [2, 2], [3, 3, 3], [4, 4, 4, 4]]

而在這個例子,我們也可以透過對 mapped 進行 flatMap 獲得同樣的結果:

實際上 some.flatMap(transform)Array(some.map(transform).joined()) 效果相同。複雜度為:O(m + n),n 為該序列長度,m 為其結果長度。

因此,你可以把 flatMap 看作一種把多層陣列攤平的方式。

⒊ compactMap(_:)

接著是這次最後一種 map 方式,稱為 compactMap ,而他的功能也是映射的一種,特別的是透過 compactMap 所映射出的元素結果都是非可選(non-nil)的值。因此,當你的希望映射結果的陣列是非可選類型的,那麼使用 compactMap 是一個適合的做法。首先我們看看以下範例:

我們可以 mappedpossibleNumber.map 所得到的結果,而 Int(str) 是可失敗的初始化器,因此該元素類型為 Int? ,所得到的結果也為 [Int?]

而如果我們使用 compactMap 做相同的操作,當 Int(str) 的結果為 nil 時,則不會被加入到陣列中,而只有在值不為 nil 時才會被添加到陣列中。

當然我們也可以直接對 mappedcompactMap 的操作:

複雜度:O(m + n),其中 n 為該序列的長度, m 為結果的長度。

因此你可以把 compactMap 的操作視為一種映射出非可選的陣列結果

⒋ reduce

在 Swift 中,如果你想要讓一個集合中所有元素轉換為一個值時,那麼使用 reduce 是一個很好的方式。而 Swift 中有兩個不同的 reduce,首先我們來看看第一種。

你可以看到這個 reduce 中有兩個參數, initialResult 顧名思義為結果的初始值(也可以理解成的累加值),其傳遞給第一次的閉包操作。而 nextParialResult 為上個閉包返回結果與序列元素組合成新的累加值,並返回結果給下一次的閉包。

這麼說可能還是有點模糊,我們來舉個簡單的加法範例:

number.reduce 被調用時,發生以下幾個步驟:

  • initialResult(這個情況下為 0)和 numbers 的第一個元素調用 nextPartialResult 閉包,返回總和 1。
  • 使用之前調用的返回值和序列中每個元素再次重複調用該閉包。
  • 序列結束後,將從閉包返回的最後一個值返回給調用者。

其複雜度:O(n),其中 n 為序列的長度。

你可以從結果了解到 reduce 的用法,但是可能對於每次調用的過程以及 nextParialResult 以及 x, y 有些模糊。這邊你可以嘗試印出每次調用 x, y 的內容,你可能使你對於 reduce 的過程可以更加的了解。

在印出 x, y 之後,你大概能夠理解 reduce 的過程,在一開始我們賦予它一個作為結果的初始值 0,而每次閉包操作的過程,都是將序列中每個元素與上一個閉包累加值進行運算,並返回結果,直到結束。以我們這個加法的範例來說就是:

  1. 初始值為 0,第一次閉包操作 0 + 1,返回值為 1
  2. 累加值為 1,第二次閉包操作 1 + 2,返回值為 3
  3. 累加值為 3,第三次閉包操作 3 + 3,返回值為 6
  4. 累加值為 6,第四次閉包操作 6 + 4,返回結果為 10

如此一來你就能理解 reduce 每次調用閉包的操作是如何了。

而接著讓我們看看另一種 reduce 函數:

這兩個 reduce 函數最大的差別就是其閉包中的 Result 以及其返回的內容為 Void,第二種 reduce 函數中的閉包將 Result 視為 inout 參數使用。因此如果使用這種 reduce 方法實作上述加法範例如下:

第二種 reduce 調用因為其 x 為 inoutInt ,而其返回結果為 () 也就是 Void ,所以你想要達成相同的結果你必須將結果再次賦予給 x,而因為 xinout 參數,所以進行上述賦值的操作。

⚠️ Apple 在這個 reduce(into:_:) 文件上表示:

當結果為寫入時複製 (copy-on-write) 的類型時,例如 Array 或 Dictionary 時,此方法優於 reduce(_:_:) 來提高效率。

接著讓我們看看另外一個例子:

這個例子中 letters.reduce 中的 counts 在最一開始為 [:],而每次的閉包操作都是將 counts[letter] 的值加 1,預設為 0。而每一次調用 counts 都可以視為同一個對象(為 inout 參數),因此每次調用的對象都是同一個 Dictionary。而最後 letterCount 的輸出結果為每個單字出現的次數,也就是我們 counts 的最終結果。

其複雜度也與上述 reduce(_:_:) 相同,O(n)。

✎ 後記:

那麼這次關於轉換陣列的介紹就到這邊了,希望各位讀者能夠活用上面提到的 mapflatMapcompactMap 以及 reduce ,尤其是這幾種 map 的方式,當你需要對一整個集合執行一些特殊轉換時非常好用,有點像是一個 Adpater 的轉換過程。

題外話:寫到一半才想起來 Collection Types 都有這些方法,因為比較常使用陣列做這些事情,一不小心就把他歸類在 Array 裡面了… 😂,之後應該會針對不重複的函數繼續說明。

--

--

Jeremy Xue
Jeremy Xue ‘s Blog

Hi, I’m Jeremy. [好想工作室 — iOS Developer]