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