将常用高阶函数与集合结合使用

1. 简介

通过在 Kotlin 中使用函数类型和 lambda 表达式 Codelab,您学习了高阶函数(即接受其他函数作为参数和/或返回函数的函数),例如 repeat()。高阶函数与集合密切相关,因为它们让您用较少的代码即可完成常见任务(例如排序或过滤)。现在,您已经为使用集合打下了坚实的基础,是时候回顾一下高阶函数了。

在此 Codelab 中,您将了解可对集合类型使用的各种函数,包括 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() 中,使用尾随 lambda 语法对 cookies 列表调用 forEach()。由于尾随 lambda 是唯一实参,因此在调用函数时可以省略括号。
fun main() {
    cookies.forEach {

    }
}
  1. 在 lambda 正文中,添加一个输出 itprintln() 语句。
fun main() {
    cookies.forEach {
        println("Menu item: $it")
    }
}
  1. 运行代码并观察输出结果。输出结果仅包含类型名称 (Cookie) 以及对象的唯一标识符,而不包含对象的内容。
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 和对象的唯一标识符,后跟 .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() 可将 List<Cookie> 转换为仅包含饼干 nameList<String>,前提是您要告知 map() 函数如何从每个 Cookie 项创建 String

e0605b7b09f91717.png

假设您正在编写一款应用,它可以显示面包店的互动式菜单。当用户进入用于显示饼干菜单的屏幕后,他们可能想要查看按合乎逻辑的方式呈现的数据,例如名称后跟价格。您可以使用 map() 函数来创建使用相关数据(名称和价格)进行格式化设置的字符串列表。

  1. main() 中移除之前的所有代码。创建一个名为 fullMenu 的新变量,并将其设为与对 cookies 列表调用 map() 的结果相等。
val fullMenu = cookies.map {

}
  1. 在 lambda 的正文中,添加一个格式化为包含 itnameprice 的字符串。
val fullMenu = cookies.map {
    "${it.name} - $${it.price}"
}
  1. 输出 fullMenu 的内容。您可以使用 forEach() 来实现此目的。从 map() 返回的 fullMenu 集合的类型是 List<String>,而不是 List<Cookie>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

4. filter()

借助 filter() 函数,您可以创建集合的子集。例如,如果您有一个数字列表,则可以使用 filter() 来创建一个新列表,使其仅包含可被 2 整除的数字。

d4fd6be7bef37ab3.png

尽管 map() 函数的结果始终生成大小相同的集合,但 filter() 生成的集合的大小却是等于或小于原始集合的。与 map() 不同,生成的集合也具有相同的数据类型,因此过滤 List<Cookie> 将产生另一个 List<Cookie>

map()forEach() 类似,filter() 接受单个 lambda 表达式作为形参。lambda 包含代表集合中的每个项的单个形参,并会返回 Boolean 值。

对于集合中的每个项:

  • 如果 lambda 表达式的结果为 true,则表示此项包含在新集合中。
  • 如果结果为 false,则表示此项不包含在新集合中。

如果您想获取应用中的部分数据,这会非常有用。例如,假设面包店想在菜单的单独版块中重点推广其软饼干。您可以先对 cookies 列表应用 filter(),然后再输出项。

  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 的结果可为 null,因此您可以使用 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 以返回作为排序依据的属性。例如,如果您想按 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. 了解详情