iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 20
0

在處理資料的時候,我們常常會先把 Collection 裡的資料取出後,再逐一轉換成另外一個類別或格式,由於 Collection 有 Iterator 的特點,所以比較阿 Q 的作法就是用 2 個 for 迴圈完成。但其實 Kotlin 的標準函式庫裡內建了一系列的轉換 Extension 可以用,也就是說,這些 Extension 可以幫你將現有的 Collection 以傳入的 Lambda 做轉換,效率++!

把元素轉換成其他東西

map() 讓我們可以把 Collection 裡面的元素一個一個拿出來,然後套用一個 Lambda 讓它轉換成其他東西。就行為來說,map()forEach() 都會逐一碰過元素一遍,不過差異就在 map() 會把轉換的過程回傳而 forEach() 不會。另外一個特點就是,map() 回傳的 Collection 一定會和原本的 Collection 一樣大。

val numbers = listOf(1, 2, 3)
numbers.map { it * it } // [1, 4, 9]

假如取出元素時,想要一併取出 index 做處理,標準函式庫裡也有 mapIndexed() 可以使用,讓操作更彈性一些。

val numbers = listOf(1, 2, 3)
numbers.mapIndexed { index, value ->
    "$index: $value"
}
// [0: 1, 1: 2, 2: 3] 

假如 Lambda 操作結果有可能是 null 的話,我們可以用 mapNotNull()mapIndexedNotNull() 來過濾,就不需自行再用 filterNotNull() 額外處理。

val listOfNumbers = listOf(1, 2, 3)
listOfNumbers.mapNotNull {
    if ( it == 2) null else it * 3
} // [3, 9]

listOfNumbers.mapIndexedNotNull { index, value ->
    if (index == 0) null else value * index
} // [2, 6]

把兩個 Collection 黏起來

當我們需要把兩個 Collection 的元素做成對照表時,可以用 zip(),它會將兩個 Collection 裡相同位置的元素做成 Pair 並回傳 List。你可以想像就是把兩個 Collection 像是用拉鏈一樣把兩邊黏起來一樣。

val colors = listOf("red", "brown", "grey")
val animals = listOf("fox", "bear", "wolf")
colors.zip(animals) // [(red, fox), (brown, bear), (grey, wolf)]

大多情況我們都會在兩個一樣大的 Collection 上套用 zip() 做 Pair。假如兩個 Collection 不一樣大的話,則會以小的為基準,多餘的都會被捨棄。

val animals = listOf("fox", "bear", "wolf")
val twoAnimals = listOf("fox", "bear")
colors.zip(twoAnimals) // [(red, fox), (brown, bear)]
twoAnimals.zip(colors) // [(fox, red), (bear, brown)]

有趣的是,zip() 有實作 infix function,所以你也可以把 zip 像關鍵字一樣地使用。

colors zip animals // [(red, fox), (brown, bear), (grey, wolf)]

zip() 還可以傳入 Lambda 做第二個參數,讓我們可以取得合併完後的每一個 Pair 內容做二次處理,這樣的彈性讓我們可以再少一步。

val colors = listOf("red", "brown", "grey")
val animals = listOf("fox", "bear", "wolf")

// 傳入 Lambda 做第二個參數,取得 Pair 裡的 color 和 animal
colors.zip(animals) { color, animal -> "The ${animal.capitalize()} is $color"}
// 回傳 [The Fox is red, The Bear is brown, The Wolf is grey]

// 同樣的結果用 `zip()` 和 `map()` 達成  
colors.zip(animals).map {
    "The ${it.second.capitalize()} is ${it.first}"
}
// 回傳 [The Fox is red, The Bear is brown, The Wolf is grey]

既然可以把兩個 Collection 黏在一起,當然就可以反過來拆開。假如你的 Collection 裡放了許多 Pair,就可以用 unzip() 把 key 和 value 各自拆成 List,然後再以 Pair 回傳。

val numberPairs = listOf("one" to 1, "two" to 2, "three" to 3, "four" to 4)
numberPairs.unzip() // ([one, two, three, four], [1, 2, 3, 4])

將處理過的結果做關聯

假如我們想要將 Collection 的元素與處理過的結果做對照,,可以用 associate 開頭的 method。它們可以將原本 Collection 的 key 或 value 跟 Lambda 處理過的結果做成 Map

associateWith() 會將原本 Collection 裡的元素當成 key,把 Lambda 處理過的結果當成 value 組合成 Map。不過要注意的是,associateWith() 會把重覆的元素去掉,回傳的 Map 裡只留下最後一個不重覆的元素。

val numbers = listOf("one", "two", "two", "three", "four")
numbers.associateWith { it.length } // {one=3, two=3, three=5, four=4}

假如想要反過來,把原本 Collection 裡的元素當成 value,那就改用 associateBy()。或是你可以傳入 2 個 Lambda,keySelector 處理 key、valueTransform 處理 value。

val numbers = listOf("one", "two", "three", "four")

numbers.associateBy { it.first().toUpperCase() } // {O=one, T=three, F=four}
numbers.associateBy(
    keySelector = { it.first().toUpperCase() },
    valueTransform = { it.length }
) // {O=3, T=5, F=4}

associate() 的作法則是將 Collection 的元素傳給 Lambda,Lambda 則要回傳一個 Pair,最後會將這成群的 Pair 轉成 Map 回傳。

val names = listOf("Alice Adams", "Brian Brown", "Clara Campbell")
names.associate {
    it.split(" ").let { (firstName, lastName) -> lastName to firstName }
}
// {Adams=Alice, Brown=Brian, Campbell=Clara}

將巢狀結構平面化

假如你的 Collection 是巢狀的(比方說 List 裡面裝了數個 List),想要把 Collection 的階層打平,並取回各階層的元素成一個單層的 List 的話,flatten() 就是你要找的!

val numberSets = listOf(setOf(1, 2, 3), setOf(4, 5, 6), setOf(1, 2))
numberSets.flatten() // [1, 2, 3, 4, 5, 6, 1, 2]

flatMap() 則提供更彈性的巢狀 Collection 處理機制,它將傳入的 Lambda 套用在元素上以產生另一個 Collection,最後會再把階層打平回傳一個只有一層的 List。行為有點類似綜合運用了 flatten()map()

data class StringContainer(val values: List<String>)

val containers = listOf(
    StringContainer(listOf("one", "two", "three")),
    StringContainer(listOf("four", "five", "six")),
    StringContainer(listOf("seven", "eight"))
)

containers.flatMap { it.values } // [one, two, three, four, five, six, seven, eight]

轉換成字串

若需要將 Collection 的內容印出來方便閱讀,可以用 joinToString() 來合併成單一 String。可以注意到,joinToString()toString() 有點像但輸出的格式不同。

val numbers = listOf("one", "two", "three", "four")

numbers.joinToString() // one, two, three, four
numbers.toString() // [one, two, three, four]

若是要調整輸出的格式,joinToString() 可以傳入 separatorprefixpostfix 等參數,在輸出的時候,會先輸出 prefix,接著輸出每個元素並以 separator 隔開,最後再以 postfix 結束。

val numbers = listOf("one", "two", "three", "four")    
numbers.joinToString(separator = " | ", prefix = "start: ", postfix = ": end") // start: one | two | three | four: end

假如 Collection 的內容很大,我們可能只需要印出前面幾個代表性的元素,後面就用「…」顯示。則可以 limit 設定輸出的個數、truncated 設定超過個數時要用什麼字串做結尾。因為 limittruncatedjoinToString() 的第 4-5 個參數,若前面的參數都不設定的話,記得要用顯示指定參數名稱。

val numbers = listOf("one", "two", "three", "four")
numbers.joinToString(limit = 2, truncated = "...") // one, two, ...

joinToString() 甚至還有第 6 個參數,你可以傳入一個 Lambda 決定要怎麼轉換成字串。

val numbers = listOf("one", "two", "three", "four")
numbers.joinToString { "Element: ${it.toUpperCase()}"} // Element: ONE, Element: TWO, Element: THREE, Element: FOUR

假如是要把轉換好的字串加到其他字串裡的話,則要改用 joinTo()。要傳入的參數記得要是一個 Appendable 物件。


val numbers = listOf("one", "two", "three", "four")
val listString = StringBuffer("The list of numbers: ")
numbers.joinTo(listString) // The list of numbers: one, two, three, four

表格整理

在這個章節裡,我們實驗了許多 Collection 轉換的方式。熟悉這些 method 後,對於用 Kotlin 處理資料將會更加上手。許多 Kotlin 高手之所以程式碼可以如此簡潔,也是因為能將這些技巧發揮到極致的關係。為了一覽這些 API 在不同 Collection 上的行為,以下用表格來整理本章所討論到的 method:

行為 Array List Set Map
map() 轉換元素 v v v v
mapIndexed() 轉換元素及索引 v v v v
mapNotNull() 轉換元素後排除 null v v v v
mapIndexedNotNull() 轉換元素及索引後排除 null v v v v
zip() 將兩個集合相黏 v v v v
unzip() 將 Pair 拆開 v v v v
flatten() 將集合平面化 v v v v
flatMap() 依 Lambda 把集合平面化 v v v v
joinToString() 把元素合併成字串 v v v v
joinTo() 把元素整併到別的字串 v v v v

參考資料


上一篇
第十九天:Collection 操作之迴圈
下一篇
第二十一天:Collection 操作之聚合
系列文
新手也能懂的 Kotlin Collection 賞玩門道31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言