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()
用于按指定的属性对集合进行排序。