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,并使用高阶函数以不同方式设置此菜单的格式。
首先要设置初始代码。
- 前往 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()中,使用尾随 lambda 语法对cookies列表调用forEach()。由于尾随 lambda 是唯一实参,因此在调用函数时可以省略括号。
fun main() {
cookies.forEach {
}
}
- 在 lambda 正文中,添加一个输出
it的println()语句。
fun main() {
cookies.forEach {
println("Menu item: $it")
}
}
- 运行代码并观察输出结果。输出结果仅包含类型名称 (
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
在字符串中嵌入表达式
最初了解字符串模板时,您见到过如何将美元符号 ($) 与变量名称结合使用以将变量名称插入字符串。不过,当与点运算符 (.) 结合以访问属性时,这种方法就无法实现预期效果了。
- 在对
forEach()的调用中,修改 lambda 的正文以将$it.name插入字符串。
cookies.forEach {
println("Menu item: $it.name")
}
- 运行您的代码。请注意,这会插入类名称、
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
如需访问属性并将其嵌入字符串,您需要一个表达式。您可以用大括号将表达式括住,使其成为字符串模板的一部分。

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() 可将 List<Cookie> 转换为仅包含饼干 name 的 List<String>,前提是您要告知 map() 函数如何从每个 Cookie 项创建 String。

假设您正在编写一款应用,它可以显示面包店的互动式菜单。当用户进入用于显示饼干菜单的屏幕后,他们可能想要查看按合乎逻辑的方式呈现的数据,例如名称后跟价格。您可以使用 map() 函数来创建使用相关数据(名称和价格)进行格式化设置的字符串列表。
- 从
main()中移除之前的所有代码。创建一个名为fullMenu的新变量,并将其设为与对cookies列表调用map()的结果相等。
val fullMenu = cookies.map {
}
- 在 lambda 的正文中,添加一个格式化为包含
it的name和price的字符串。
val fullMenu = cookies.map {
"${it.name} - $${it.price}"
}
- 输出
fullMenu的内容。您可以使用forEach()来实现此目的。从map()返回的fullMenu集合的类型是List<String>,而不是List<Cookie>。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
4. filter()
借助 filter() 函数,您可以创建集合的子集。例如,如果您有一个数字列表,则可以使用 filter() 来创建一个新列表,使其仅包含可被 2 整除的数字。

尽管 map() 函数的结果始终生成大小相同的集合,但 filter() 生成的集合的大小却是等于或小于原始集合的。与 map() 不同,生成的集合也具有相同的数据类型,因此过滤 List<Cookie> 将产生另一个 List<Cookie>。
与 map() 和 forEach() 类似,filter() 接受单个 lambda 表达式作为形参。lambda 包含代表集合中的每个项的单个形参,并会返回 Boolean 值。
对于集合中的每个项:
- 如果 lambda 表达式的结果为
true,则表示此项包含在新集合中。 - 如果结果为
false,则表示此项不包含在新集合中。
如果您想获取应用中的部分数据,这会非常有用。例如,假设面包店想在菜单的单独版块中重点推广其软饼干。您可以先对 cookies 列表应用 filter(),然后再输出项。
- 在
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的结果可为 null,因此您可以使用 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 以返回作为排序依据的属性。例如,如果您想按 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()用于按指定属性对集合进行排序。