含有集合的高階函式

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 代表烘焙坊的餅乾菜單 (超好吃!),並使用高階函式,以不同方式設定菜單格式。

首先,設定初始程式碼。

  1. 前往 Kotlin Playground
  2. main() 函式上方新增 Cookie 類別。每個 Cookie 的例項都代表菜單上的一個項目,並有 nameprice 及其他餅乾相關資訊。
class Cookie(
    val name: String,
    val softBaked: Boolean,
    val hasFilling: Boolean,
    val price: Double
)

fun main() {

}
  1. 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 清單的項目。

  1. main() 內,在 cookies 清單上使用結尾 lambda 語法呼叫 forEach()。因為結尾 lambda 是唯一的引數,因此您可以在呼叫函式時省略括弧。
fun main() {
    cookies.forEach {

    }
}
  1. 在 lambda 內文新增顯示 itprintln() 陳述式。
fun main() {
    cookies.forEach {
        println("Menu item: $it")
    }
}
  1. 執行程式碼,看看輸出結果。輸出內容就是類型名稱 (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

在字串內嵌入運算式

當您剛開始認識字串範本的時候,您曾經看到美元符號 ($) 如何當做變數名稱並插入字串內。不過,這個方法如果跟點號運算子 (.) 一起使用,並不能照預期的方式取屬性。

  1. 修改 forEach() 呼叫內容的 lambda 內文,將 $it.name 插入字串。
cookies.forEach {
    println("Menu item: $it.name")
}
  1. 執行程式碼。請注意,這樣會插入類別名稱 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

如果想存取屬性並嵌入字串內,您就需要使用運算式。只要在運算式前後加上大括弧,就能讓運算式成為字串範本的一部分。

2c008744cee548cc.png

lambda 運算式位於左右大括弧中間。您可以存取屬性、執行數學運算及呼叫函式等等,系統會將 lambda 回傳值插入字串。

我們可以修改程式碼,以將名稱插入字串。

  1. it.name 前後加上大括弧,使其成為 lambda 運算式。
cookies.forEach {
    println("Menu item: ${it.name}")
}
  1. 執行程式碼。輸出結果內有每個 Cookiename
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 項目建立 Stringmap() 即可將 List<Cookie> 轉換為只含有餅乾 name 資訊的 List<String>

e0605b7b09f91717.png

假設您編寫的應用程式要能顯示某間烘焙坊的互動式菜單。當使用者進入顯示餅乾菜單的畫面時,會希望看到資料用有邏輯的方式呈現,例如在名稱後面顯示價錢。您可以建立一個字串清單,並使用 map() 函式以相關資料 (名稱和價錢) 設定格式。

  1. 移除 main() 先前所有的程式碼。建立名為 fullMenu 的新變數,並讓其等於在 cookies 清單呼叫 map() 的結果。
val fullMenu = cookies.map {

}
  1. 在 lambda 內文中加入字串,並遵守含有 nameitprice 等資訊的格式。
val fullMenu = cookies.map {
    "${it.name} - $${it.price}"
}
  1. 顯示 fullMenu 的內容。您可以用 forEach() 達到此效果。map() 回傳的 fullMenu 集合並未使用 List<Cookie>,而是類型 List<String>cookies 裡的每個 Cookie 都可以對應 fullMenu 裡的 String
println("Full menu:")
fullMenu.forEach {
    println(it)
}
  1. 執行程式碼。輸出結果會和 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 除盡的數字。

d4fd6be7bef37ab3.png

map() 函式的結果一律會得到相同大小的集合,而 filter() 產生的集合和原集合相比,大小不是相同就是更小。和 map() 不同之處在於,結果產生的集合也會有相同的資料類型,所以篩選 List<Cookie> 會產生另一個 List<Cookie>

map()forEach() 相同,filter() 的參數也採用單一 lambda 運算式。lambda 的單一參數代表集合內的每一個項目,並會回傳 Boolean 值。

集合內的每一個項目:

  • 如果 lambda 運算式的結果是 true,那麼項目會在新的集合內。
  • 如果結果是 false,那麼項目不會在新的集合內。

如果您想取得應用程式中的部分資料,這種做法就能派上用場。比如,烘焙坊可能會想在菜單中的獨立部分主打軟餅乾。您可以先 filter() cookies 清單,然後再顯示項目。

  1. main() 內建立名為 softBakedMenu 的新變數,然後設定為在 cookies 清單呼叫 filter() 的結果。
val softBakedMenu = cookies.filter {
}
  1. 在 lambda 內文加入布林值運算式,檢查餅乾的 softBaked 屬性是否等於 true。因為 softBaked 本身就是 Boolean,因此 lambda 內文只需要含有 it.softBaked
val softBakedMenu = cookies.filter {
    it.softBaked
}
  1. 使用 forEach() 顯示 softBakedMenu 內容。
println("Soft cookies:")
softBakedMenu.forEach {
    println("${it.name} - $${it.price}")
}
  1. 執行程式碼。這個菜單的顯示方式和之前一樣,但是只會列出軟餅乾。
...
Soft cookies:
Banana Walnut - $1.49
Snickerdoodle - $1.39
Blueberry Tart - $1.79

5. groupBy()

您可以運用 groupBy() 函式,根據函式將清單轉換成對應項目。該函式每個專屬回傳值都會成為產生對應項目中的鍵。每個鍵的值都是產生該專屬回傳值的集合中的項目。

54e190b34d9921c0.png

這些鍵的資料類型都和傳遞到 groupBy() 的函式的傳回類型相同。這些值的資料類型都是來自原始清單的項目清單。

因為這個內容不好理解,所以請看一個簡單的範例。使用和之前一樣的數字清單,將其中的數字按奇偶分組。

您可以讓這些數字除以 2,然後檢查餘數是 0 還是 1,藉此檢查每個數字是奇數還是偶數。如果餘數是 0,表示這個數字是偶數。而如果餘數是 1,就表示數字是奇數。

您可以用模數運算子 (%) 達到這個目的。模數運算子會把被除數分隔在運算式左邊,除以右邊的除數。

4c3333da9e5ee352.png

模數運算子不會像除法運算子 (/) 一樣傳回除式結果,而是會傳回餘數。您可以藉此檢查數字是偶數還是奇數。

4219eacdaca33f1d.png

系統會用以下的 lambda 運算式呼叫 groupBy() 函式:{ it % 2 }

產生的對應項目有兩組鍵:01。每個鍵都有 List<Int> 類型的值。0 鍵的清單內只有偶數,而 1 鍵的清單內只有奇數。

至於現實生活的用途,可能是依照拍攝主題或地點分組的相片應用程式。在我們的烘焙坊菜單裡,我們可以依餅乾是否為軟餅乾,對菜單內容進行分組。

使用 groupBy() 根據 softBaked 屬性分類菜單內容。

  1. 移除上個步驟的 filter() 呼叫。

要移除的程式碼

val softBakedMenu = cookies.filter {
    it.softBaked
}
println("Soft cookies:")
softBakedMenu.forEach {
    println("${it.name} - $${it.price}")
}
  1. cookies 清單呼叫 groupBy(),儲存在名為 groupedMenu 的變數裡。
val groupedMenu = cookies.groupBy {}
  1. 傳遞到會回傳 it.softBaked 的 lambda 運算式。傳回類型為 Map<Boolean, List<Cookie>>
val groupedMenu = cookies.groupBy { it.softBaked }
  1. 建立含有 groupedMenu[true] 值的 softBakedMenu 變數,以及含有 groupedMenu[false] 值的 crunchyMenu 變數。訂閱 Map 的結果可為空值,因此您可以使用 Elvis 運算子 (?:) 傳回空白清單。
val softBakedMenu = groupedMenu[true] ?: listOf()
val crunchyMenu = groupedMenu[false] ?: listOf()
  1. 加入可以顯示軟餅乾菜單的程式碼,後面加上脆餅乾的菜單。
println("Soft cookies:")
softBakedMenu.forEach {
    println("${it.name} - $${it.price}")
}
println("Crunchy cookies:")
crunchyMenu.forEach {
    println("${it.name} - $${it.price}")
}
  1. 執行程式碼。使用 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() 函式從集合產生單一值。這通常會用來計算總金額,或將清單內的所有元素加總,以求出平均值。

a9e11a1aad05cb2f.png

fold() 函式使用兩個參數:

  • 初始值。系統會在呼叫函式時推論資料類型 (也就是推論 0 的初始值為 Int)。
  • 可以回傳和初始值相同類型的值的 lambda 運算式。

這個 lambda 運算式還有兩個參數:

  • 第一個參數就是所謂的累計值。和初始值使用相同的資料類型。您可以把他當做執行總量。每次呼叫 lambda 運算式的時候,累計值都和上次呼叫 lambda 的回傳值相同。
  • 第二個參數和集合內的所有元素使用相同的類型。

跟其他您看過的函式一樣,集合裡的每個元素都會呼叫這個 lambda 運算式,引此使用 fold() 即可簡單加總所有元素。

讓我們使用 fold() 計算所有餅乾的總金額。

  1. main() 中建立名為 totalPrice 的新變數,並將其設為等同對 cookies 清單呼叫 fold() 的結果。傳遞 0.0 當做初始值。系統推論類型為 Double
val totalPrice = cookies.fold(0.0) {
}
  1. 您需要為該 lambda 運算式指定全部兩種參數。使用 total 做為累計值,並使用 cookie 當做收集元素。在參數清單後方使用箭號 (->)。
val totalPrice = cookies.fold(0.0) {total, cookie ->
}
  1. 在 lambda 內文計算 totalcookie.price 的總和。系統會將其推論為回傳值,並傳遞當做下次呼叫 lambda 的 total
val totalPrice = cookies.fold(0.0) {total, cookie ->
    total + cookie.price
}
  1. 顯示 totalPrice 的值,並採用字串格式,以便判讀理解。
println("Total price: $${totalPrice}")
  1. 執行程式碼。結果應該和 cookies 清單的總金額相等。
...
Total price: $10.83

7. sortedBy()

當您剛開始學習集合時,您會學到 sort() 函式可以用來排序元素。不過,Cookie 物件集合無法使用此方法。Cookie 類別擁有多種屬性,Kotlin 不知道您要用哪一種屬性 (nameprice 等) 排序。

Kotlin 集合針對這種情形提供的就是 sortedBy() 函式。您可以用 sortedBy() 指定一個 lambda,並用這個 lambda 回傳的屬性當做排序依據。舉例來說,如果您想用 price 排序,lambda 就會回傳 it.price。只要值的資料類型使用自然排列順序,也就是字串按照字母順序排序、數值使用遞增順序排列,那麼就可以按照該類型的集合排序。

5fce4a067d372880.png

您將使用 sortedBy() 按照字母順序排列餅乾清單順序。

  1. main() 現有的程式碼後方加入名為 alphabeticalMenu 的新變數,並將其設定為和在 cookies 清單呼叫 sortedBy() 相同。
val alphabeticalMenu = cookies.sortedBy {
}
  1. 在 lambda 運算式中回傳 it.name。結果產生的清單還是 List<Cookie> 類型,但是會按照 name 排序。
val alphabeticalMenu = cookies.sortedBy {
    it.name
}
  1. 顯示 alphabeticalMenu 內的餅乾名稱。您可以用 forEach(),即可在顯示每個名稱時都使用新的一行。
println("Alphabetical menu:")
alphabeticalMenu.forEach {
    println(it.name)
}
  1. 執行程式碼。按照字母順序顯示的餅乾名稱。
...
Alphabetical menu:
Banana Walnut
Blueberry Tart
Chocolate Chip
Chocolate Peanut Butter
Snickerdoodle
Sugar and Sprinkles
Vanilla Creme

8. 結語

恭喜!您剛剛看到的幾個範例已經說明了如何搭配集合使用高階函式。排序和篩選這類常見作業可使用一行程式碼執行,讓您的程式更簡潔、更清楚。

摘要

  • 您可以用 forEach() 為某集合的所有元素建立迴圈。
  • 您可以在字串內插入運算式。
  • 您可以使用 map() 為集合裡的項目套用格式,通常可以做為其他資料類型的集合。
  • filter() 可以為某集合產生子集。
  • groupBy() 可以根據函式的回傳值分割集合。
  • fold() 可以把集合轉換為單一值。
  • sortedBy() 可以按照特定屬性排序集合。

9. 瞭解詳情