Swift — Transforming an Array
讓我們對你的陣列進行一些轉換吧!
✎ 前言:
常常我們在面對一連串的資料(也就是陣列)時,我們總是會對他進行一些操作,無論你是使用 for-in
、forEach
、filter
、map
等等…。我們不外乎都是想要透過這些方法來將資料轉換或改變成我們想要的結果。
而這篇文章主要會介紹的是有關於「轉換陣列」的相關內容,這幾種方式往往透過映射(mapping)的方式將原始陣列,轉換成你所想要的結果,而大致上會圍繞在以下幾個函數和屬性中:
map
flatMap
compactMap
reduce
lazy
✏︎ 轉換陣列:
⒈ map(_:)
通常我們會使用到 map 函數的原因,通常是以下幾種:
- 從某個類型的陣列轉換成另外一個類型的陣列
- 透過
map
返回一個將原始元素進行轉換後的陣列
首先我們先來實作上述第 1 點的內容:
首先 lowercaseNames
是 cast
映射後的返回結果,而 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
使用 flatMap
與 map
閉包,但是經由 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
是一個適合的做法。首先我們看看以下範例:
我們可以 mapped
是 possibleNumber.map
所得到的結果,而 Int(str)
是可失敗的初始化器,因此該元素類型為 Int?
,所得到的結果也為 [Int?]
。
而如果我們使用 compactMap
做相同的操作,當 Int(str)
的結果為 nil
時,則不會被加入到陣列中,而只有在值不為 nil
時才會被添加到陣列中。
當然我們也可以直接對 mapped
做 compactMap
的操作:
複雜度: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,而每次閉包操作的過程,都是將序列中每個元素與上一個閉包累加值進行運算,並返回結果,直到結束。以我們這個加法的範例來說就是:
- 初始值為 0,第一次閉包操作 0 + 1,返回值為 1
- 累加值為 1,第二次閉包操作 1 + 2,返回值為 3
- 累加值為 3,第三次閉包操作 3 + 3,返回值為 6
- 累加值為 6,第四次閉包操作 6 + 4,返回結果為 10
如此一來你就能理解 reduce
每次調用閉包的操作是如何了。
而接著讓我們看看另一種 reduce
函數:
這兩個 reduce
函數最大的差別就是其閉包中的 Result
以及其返回的內容為 Void
,第二種 reduce
函數中的閉包將 Result
視為 inout
參數使用。因此如果使用這種 reduce
方法實作上述加法範例如下:
第二種 reduce
調用因為其 x 為 inout
的 Int
,而其返回結果為 ()
也就是 Void
,所以你想要達成相同的結果你必須將結果再次賦予給 x
,而因為 x
為 inout
參數,所以進行上述賦值的操作。
⚠️ Apple 在這個 reduce(into:_:)
文件上表示:
當結果為寫入時複製 (copy-on-write) 的類型時,例如 Array 或 Dictionary 時,此方法優於 reduce(_:_:) 來提高效率。
接著讓我們看看另外一個例子:
這個例子中 letters.reduce
中的 counts
在最一開始為 [:]
,而每次的閉包操作都是將 counts[letter]
的值加 1,預設為 0。而每一次調用 counts
都可以視為同一個對象(為 inout
參數),因此每次調用的對象都是同一個 Dictionary
。而最後 letterCount
的輸出結果為每個單字出現的次數,也就是我們 counts
的最終結果。
其複雜度也與上述
reduce(_:_:)
相同,O(n)。
✎ 後記:
那麼這次關於轉換陣列的介紹就到這邊了,希望各位讀者能夠活用上面提到的 map
、flatMap
、compactMap
以及 reduce
,尤其是這幾種 map 的方式,當你需要對一整個集合執行一些特殊轉換時非常好用,有點像是一個 Adpater 的轉換過程。
題外話:寫到一半才想起來 Collection Types 都有這些方法,因為比較常使用陣列做這些事情,一不小心就把他歸類在 Array 裡面了… 😂,之後應該會針對不重複的函數繼續說明。
✎ 參考文欓:
https://developer.apple.com/documentation/swift/array/3017522-map
https://developer.apple.com/documentation/swift/array/3126947-flatmap
https://developer.apple.com/documentation/swift/array/2957701-compactmap
https://developer.apple.com/documentation/swift/array/2298686-reduce
https://developer.apple.com/documentation/swift/array/3126956-reduce