集合中的高阶函数

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,并使用高阶函数以不同方式格式化菜单。

首先设置初始代码。

  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() 接受单个 action 参数,该参数是类型为 (T) -> Unit 的函数。

T 对应于集合包含的任何数据类型。由于 lambda 接受单个参数,您可以省略名称并使用 it 引用该参数。

使用 forEach() 函数打印 cookies 列表中的项目。

  1. main() 中,使用 trailing lambda 语法在 cookies 列表上调用 forEach()。因为 trailing 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 并检查余数是否为 01 来检查它是奇数还是偶数。如果余数为 0,则数字为偶数。否则,如果余数为 1,则数字为奇数。

这可以通过取模运算符 (%) 实现。取模运算符将表达式左侧的被除数除以右侧的除数。

4c3333da9e5ee352.png

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

4219eacdaca33f1d.png

groupBy() 函数使用以下 lambda 表达式调用:{ 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 进行下标操作的结果可能为空,您可以使用 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,该 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. 了解更多