1. 简介
在 在 Kotlin 中使用函数类型和 lambda 表达式 Codelab 中,您学习了高阶函数,即接受其他函数作为参数和/或返回函数的函数,例如 repeat()。高阶函数与集合尤其相关,因为它们可以帮助您用更少的代码执行常见的任务,例如排序或过滤。现在您已经打下了坚实的集合基础,是时候回顾一下高阶函数了。
在本 Codelab 中,您将学习可用于集合类型的各种函数,包括 forEach()、map()、filter()、groupBy()、fold() 和 sortedBy()。在此过程中,您将获得使用 lambda 表达式的额外练习。
前提条件
- 熟悉函数类型和 lambda 表达式。
- 熟悉 trailing 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() 接受单个 action 参数,该参数是类型为 (T) -> Unit 的函数。
T 对应于集合包含的任何数据类型。由于 lambda 接受单个参数,您可以省略名称并使用 it 引用该参数。
使用 forEach() 函数打印 cookies 列表中的项目。
- 在
main()中,使用 trailing lambda 语法在cookies列表上调用forEach()。因为 trailing 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,则数字为奇数。
这可以通过取模运算符 (%) 实现。取模运算符将表达式左侧的被除数除以右侧的除数。

取模运算符不像除法运算符 (/) 那样返回除法结果,而是返回余数。这使得它对于检查数字是偶数还是奇数非常有用。

groupBy() 函数使用以下 lambda 表达式调用:{ 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()用于按指定的属性对集合进行排序。