1. 簡介
透過「在 Kotlin 中使用函式類型和 lambda 運算式」程式碼研究室,您已瞭解高階函式的概念,這類函式會將其他函式做為參數,並/或傳回 repeat()
等函式。高階函式和集合關係非常密切,可以幫助您用更少量的程式碼執行排序或篩選這類一般工作。現在您既然已經擁有使用集合的基礎概念,就該是複習高階函式的時候了。
在本程式碼研究室中,您將會學習如何在集合類型內運用各種函數,包括 forEach()
、map()
、filter()
、groupBy()
、fold()
以及 sortedBy()
。在過程中,您還會額外練習到如何使用 lambda 運算式。
必要條件
- 熟悉函式類型和 lambda 運算式。
- 熟悉結尾的 lambda 語法,例如
repeat()
函式結尾。 - Kotlin 各種集合類型的知識,例如
List
。
課程內容
- 如何在字串內嵌入 lambda 運算式。
- 如何和
List
集合搭配使用多種高階函式,包括forEach()
、map()
、filter()
、groupBy()
、fold()
和sortedBy()
。
軟硬體需求
- 可存取 Kotlin Playground 的網路瀏覽器。
2. forEach() 和含有 lambda 的字串範本
範例程式碼
在以下範例中,您會用 List
代表烘焙坊的餅乾菜單 (超好吃!),並使用高階函式,以不同方式設定菜單格式。
首先,設定初始程式碼。
- 前往 Kotlin Playground。
- 在
main()
函式上方新增Cookie
類別。每個Cookie
的例項都代表菜單上的一個項目,並有name
、price
及其他餅乾相關資訊。
class Cookie(
val name: String,
val softBaked: Boolean,
val hasFilling: Boolean,
val price: Double
)
fun main() {
}
- 在
Cookie
類別下方的main()
以外的地方建立餅乾清單,如下所示。系統會推論類型為List<Cookie>
。
class Cookie(
val name: String,
val softBaked: Boolean,
val hasFilling: Boolean,
val price: Double
)
val cookies = listOf(
Cookie(
name = "Chocolate Chip",
softBaked = false,
hasFilling = false,
price = 1.69
),
Cookie(
name = "Banana Walnut",
softBaked = true,
hasFilling = false,
price = 1.49
),
Cookie(
name = "Vanilla Creme",
softBaked = false,
hasFilling = true,
price = 1.59
),
Cookie(
name = "Chocolate Peanut Butter",
softBaked = false,
hasFilling = true,
price = 1.49
),
Cookie(
name = "Snickerdoodle",
softBaked = true,
hasFilling = false,
price = 1.39
),
Cookie(
name = "Blueberry Tart",
softBaked = true,
hasFilling = true,
price = 1.79
),
Cookie(
name = "Sugar and Sprinkles",
softBaked = false,
hasFilling = false,
price = 1.39
)
)
fun main() {
}
運用 forEach()
建立清單迴圈
您要學的第一個高階函式就是 forEach()
函式。forEach()
函式會針對集合內的每個項目,將傳遞來的函式當做參數執行一次。這和 repeat()
函式或 for
迴圈的運作方式相似。系統會為第一個元素執行 lambda,然後是第二個元素,以此類推,直到集合內的每個元素都執行過為止。方法簽名如下所示:
forEach(action: (T) -> Unit)
forEach()
會使用單一操作參數,是 (T) -> Unit
類型的函數。
T
可以對應集合內含的任何資料類型。由於 lambda 使用單一參數,因此您可以省略名稱,並用 it
參照參數。
使用 forEach()
函式顯示 cookies
清單的項目。
- 在
main()
內,在cookies
清單上使用結尾 lambda 語法呼叫forEach()
。因為結尾 lambda 是唯一的引數,因此您可以在呼叫函式時省略括弧。
fun main() {
cookies.forEach {
}
}
- 在 lambda 內文新增顯示
it
的println()
陳述式。
fun main() {
cookies.forEach {
println("Menu item: $it")
}
}
- 執行程式碼,看看輸出結果。輸出內容就是類型名稱 (
Cookie
),以及該物件的專屬 ID,但是並非該物件的內容。
Menu item: Cookie@5a10411 Menu item: Cookie@68de145 Menu item: Cookie@27fa135a Menu item: Cookie@46f7f36a Menu item: Cookie@421faab1 Menu item: Cookie@2b71fc7e Menu item: Cookie@5ce65a89
在字串內嵌入運算式
當您剛開始認識字串範本的時候,您曾經看到美元符號 ($
) 如何當做變數名稱並插入字串內。不過,這個方法如果跟點號運算子 (.
) 一起使用,並不能照預期的方式取屬性。
- 修改
forEach()
呼叫內容的 lambda 內文,將$it.name
插入字串。
cookies.forEach {
println("Menu item: $it.name")
}
- 執行程式碼。請注意,這樣會插入類別名稱
Cookie
和物件的專屬 ID,後面接著.name
;但不會存取name
屬性的值。
Menu item: Cookie@5a10411.name Menu item: Cookie@68de145.name Menu item: Cookie@27fa135a.name Menu item: Cookie@46f7f36a.name Menu item: Cookie@421faab1.name Menu item: Cookie@2b71fc7e.name Menu item: Cookie@5ce65a89.name
如果想存取屬性並嵌入字串內,您就需要使用運算式。只要在運算式前後加上大括弧,就能讓運算式成為字串範本的一部分。
lambda 運算式位於左右大括弧中間。您可以存取屬性、執行數學運算及呼叫函式等等,系統會將 lambda 回傳值插入字串。
我們可以修改程式碼,以將名稱插入字串。
- 在
it.name
前後加上大括弧,使其成為 lambda 運算式。
cookies.forEach {
println("Menu item: ${it.name}")
}
- 執行程式碼。輸出結果內有每個
Cookie
的name
。
Menu item: Chocolate Chip Menu item: Banana Walnut Menu item: Vanilla Creme Menu item: Chocolate Peanut Butter Menu item: Snickerdoodle Menu item: Blueberry Tart Menu item: Sugar and Sprinkles
3. map()
您可以利用 map()
函式把集合轉換為含有相同數量元素的新集合。舉例來說,只要您向 map()
函式說明如何為每個 Cookie
項目建立 String
,map()
即可將 List<Cookie>
轉換為只含有餅乾 name
資訊的 List<String>
。
假設您編寫的應用程式要能顯示某間烘焙坊的互動式菜單。當使用者進入顯示餅乾菜單的畫面時,會希望看到資料用有邏輯的方式呈現,例如在名稱後面顯示價錢。您可以建立一個字串清單,並使用 map()
函式以相關資料 (名稱和價錢) 設定格式。
- 移除
main()
先前所有的程式碼。建立名為fullMenu
的新變數,並讓其等於在cookies
清單呼叫map()
的結果。
val fullMenu = cookies.map {
}
- 在 lambda 內文中加入字串,並遵守含有
name
及it
的price
等資訊的格式。
val fullMenu = cookies.map {
"${it.name} - $${it.price}"
}
- 顯示
fullMenu
的內容。您可以用forEach()
達到此效果。map()
回傳的fullMenu
集合並未使用List<Cookie>
,而是類型List<String>
。cookies
裡的每個Cookie
都可以對應fullMenu
裡的String
。
println("Full menu:")
fullMenu.forEach {
println(it)
}
- 執行程式碼。輸出結果會和
fullMenu
清單內容相符。
Full menu: Chocolate Chip - $1.69 Banana Walnut - $1.49 Vanilla Creme - $1.59 Chocolate Peanut Butter - $1.49 Snickerdoodle - $1.39 Blueberry Tart - $1.79 Sugar and Sprinkles - $1.39
篩選
您可以利用 filter()
函式為集合建立子集。舉例來說,如果您有一份數字清單,則可以使用 filter()
建立一份新清單,其中只含可以用 2 除盡的數字。
map()
函式的結果一律會得到相同大小的集合,而 filter()
產生的集合和原集合相比,大小不是相同就是更小。和 map()
不同之處在於,結果產生的集合也會有相同的資料類型,所以篩選 List<Cookie>
會產生另一個 List<Cookie>
。
和 map()
與 forEach()
相同,filter()
的參數也採用單一 lambda 運算式。lambda 的單一參數代表集合內的每一個項目,並會回傳 Boolean
值。
集合內的每一個項目:
- 如果 lambda 運算式的結果是
true
,那麼項目會在新的集合內。 - 如果結果是
false
,那麼項目不會在新的集合內。
如果您想取得應用程式中的部分資料,這種做法就能派上用場。比如,烘焙坊可能會想在菜單中的獨立部分主打軟餅乾。您可以先 filter()
cookies
清單,然後再顯示項目。
- 在
main()
內建立名為softBakedMenu
的新變數,然後設定為在cookies
清單呼叫filter()
的結果。
val softBakedMenu = cookies.filter {
}
- 在 lambda 內文加入布林值運算式,檢查餅乾的
softBaked
屬性是否等於true
。因為softBaked
本身就是Boolean
,因此 lambda 內文只需要含有it.softBaked
。
val softBakedMenu = cookies.filter {
it.softBaked
}
- 使用
forEach()
顯示softBakedMenu
內容。
println("Soft cookies:")
softBakedMenu.forEach {
println("${it.name} - $${it.price}")
}
- 執行程式碼。這個菜單的顯示方式和之前一樣,但是只會列出軟餅乾。
... Soft cookies: Banana Walnut - $1.49 Snickerdoodle - $1.39 Blueberry Tart - $1.79
5. groupBy()
您可以運用 groupBy()
函式,根據函式將清單轉換成對應項目。該函式每個專屬回傳值都會成為產生對應項目中的鍵。每個鍵的值都是產生該專屬回傳值的集合中的項目。
這些鍵的資料類型都和傳遞到 groupBy()
的函式的傳回類型相同。這些值的資料類型都是來自原始清單的項目清單。
因為這個內容不好理解,所以請看一個簡單的範例。使用和之前一樣的數字清單,將其中的數字按奇偶分組。
您可以讓這些數字除以 2
,然後檢查餘數是 0
還是 1
,藉此檢查每個數字是奇數還是偶數。如果餘數是 0
,表示這個數字是偶數。而如果餘數是 1
,就表示數字是奇數。
您可以用模數運算子 (%
) 達到這個目的。模數運算子會把被除數分隔在運算式左邊,除以右邊的除數。
模數運算子不會像除法運算子 (/
) 一樣傳回除式結果,而是會傳回餘數。您可以藉此檢查數字是偶數還是奇數。
系統會用以下的 lambda 運算式呼叫 groupBy()
函式:{ it % 2 }
。
產生的對應項目有兩組鍵:0
和 1
。每個鍵都有 List<Int>
類型的值。0
鍵的清單內只有偶數,而 1
鍵的清單內只有奇數。
至於現實生活的用途,可能是依照拍攝主題或地點分組的相片應用程式。在我們的烘焙坊菜單裡,我們可以依餅乾是否為軟餅乾,對菜單內容進行分組。
使用 groupBy()
根據 softBaked
屬性分類菜單內容。
- 移除上個步驟的
filter()
呼叫。
要移除的程式碼
val softBakedMenu = cookies.filter {
it.softBaked
}
println("Soft cookies:")
softBakedMenu.forEach {
println("${it.name} - $${it.price}")
}
- 在
cookies
清單呼叫groupBy()
,儲存在名為groupedMenu
的變數裡。
val groupedMenu = cookies.groupBy {}
- 傳遞到會回傳
it.softBaked
的 lambda 運算式。傳回類型為Map<Boolean, List<Cookie>>
。
val groupedMenu = cookies.groupBy { it.softBaked }
- 建立含有
groupedMenu[true]
值的softBakedMenu
變數,以及含有groupedMenu[false]
值的crunchyMenu
變數。訂閱Map
的結果可為空值,因此您可以使用 Elvis 運算子 (?:
) 傳回空白清單。
val softBakedMenu = groupedMenu[true] ?: listOf()
val crunchyMenu = groupedMenu[false] ?: listOf()
- 加入可以顯示軟餅乾菜單的程式碼,後面加上脆餅乾的菜單。
println("Soft cookies:")
softBakedMenu.forEach {
println("${it.name} - $${it.price}")
}
println("Crunchy cookies:")
crunchyMenu.forEach {
println("${it.name} - $${it.price}")
}
- 執行程式碼。使用
groupBy()
函式,根據其中一種屬性的值把菜單分成兩部分。
... Soft cookies: Banana Walnut - $1.49 Snickerdoodle - $1.39 Blueberry Tart - $1.79 Crunchy cookies: Chocolate Chip - $1.69 Vanilla Creme - $1.59 Chocolate Peanut Butter - $1.49 Sugar and Sprinkles - $1.39
6. fold()
您可以使用 fold()
函式從集合產生單一值。這通常會用來計算總金額,或將清單內的所有元素加總,以求出平均值。
fold()
函式使用兩個參數:
- 初始值。系統會在呼叫函式時推論資料類型 (也就是推論
0
的初始值為Int
)。 - 可以回傳和初始值相同類型的值的 lambda 運算式。
這個 lambda 運算式還有兩個參數:
- 第一個參數就是所謂的累計值。和初始值使用相同的資料類型。您可以把他當做執行總量。每次呼叫 lambda 運算式的時候,累計值都和上次呼叫 lambda 的回傳值相同。
- 第二個參數和集合內的所有元素使用相同的類型。
跟其他您看過的函式一樣,集合裡的每個元素都會呼叫這個 lambda 運算式,引此使用 fold()
即可簡單加總所有元素。
讓我們使用 fold()
計算所有餅乾的總金額。
- 在
main()
中建立名為totalPrice
的新變數,並將其設為等同對cookies
清單呼叫fold()
的結果。傳遞0.0
當做初始值。系統推論類型為Double
。
val totalPrice = cookies.fold(0.0) {
}
- 您需要為該 lambda 運算式指定全部兩種參數。使用
total
做為累計值,並使用cookie
當做收集元素。在參數清單後方使用箭號 (->
)。
val totalPrice = cookies.fold(0.0) {total, cookie ->
}
- 在 lambda 內文計算
total
和cookie.price
的總和。系統會將其推論為回傳值,並傳遞當做下次呼叫 lambda 的total
。
val totalPrice = cookies.fold(0.0) {total, cookie ->
total + cookie.price
}
- 顯示
totalPrice
的值,並採用字串格式,以便判讀理解。
println("Total price: $${totalPrice}")
- 執行程式碼。結果應該和
cookies
清單的總金額相等。
... Total price: $10.83
7. sortedBy()
當您剛開始學習集合時,您會學到 sort()
函式可以用來排序元素。不過,Cookie
物件集合無法使用此方法。Cookie
類別擁有多種屬性,Kotlin 不知道您要用哪一種屬性 (name
、price
等) 排序。
Kotlin 集合針對這種情形提供的就是 sortedBy()
函式。您可以用 sortedBy()
指定一個 lambda,並用這個 lambda 回傳的屬性當做排序依據。舉例來說,如果您想用 price
排序,lambda 就會回傳 it.price
。只要值的資料類型使用自然排列順序,也就是字串按照字母順序排序、數值使用遞增順序排列,那麼就可以按照該類型的集合排序。
您將使用 sortedBy()
按照字母順序排列餅乾清單順序。
- 在
main()
現有的程式碼後方加入名為alphabeticalMenu
的新變數,並將其設定為和在cookies
清單呼叫sortedBy()
相同。
val alphabeticalMenu = cookies.sortedBy {
}
- 在 lambda 運算式中回傳
it.name
。結果產生的清單還是List<Cookie>
類型,但是會按照name
排序。
val alphabeticalMenu = cookies.sortedBy {
it.name
}
- 顯示
alphabeticalMenu
內的餅乾名稱。您可以用forEach()
,即可在顯示每個名稱時都使用新的一行。
println("Alphabetical menu:")
alphabeticalMenu.forEach {
println(it.name)
}
- 執行程式碼。按照字母順序顯示的餅乾名稱。
... Alphabetical menu: Banana Walnut Blueberry Tart Chocolate Chip Chocolate Peanut Butter Snickerdoodle Sugar and Sprinkles Vanilla Creme
8. 結語
恭喜!您剛剛看到的幾個範例已經說明了如何搭配集合使用高階函式。排序和篩選這類常見作業可使用一行程式碼執行,讓您的程式更簡潔、更清楚。
摘要
- 您可以用
forEach()
為某集合的所有元素建立迴圈。 - 您可以在字串內插入運算式。
- 您可以使用
map()
為集合裡的項目套用格式,通常可以做為其他資料類型的集合。 filter()
可以為某集合產生子集。groupBy()
可以根據函式的回傳值分割集合。fold()
可以把集合轉換為單一值。sortedBy()
可以按照特定屬性排序集合。