使用集合的高阶函数

1. 简介

Kotlin 中使用函数类型和 lambda 表达式 代码实验室中,您学习了高阶函数,它们是将其他函数作为参数和/或返回函数的函数,例如repeat()。高阶函数与集合特别相关,因为它们可以帮助您使用更少的代码来执行常见的任务,例如排序或过滤。现在您已经拥有了使用集合的坚实基础,是时候重新学习高阶函数了。

在本代码实验室中,您将学习可在集合类型上使用的各种函数,包括forEach()map()filter()groupBy()fold()sortedBy()。在此过程中,您将获得更多使用 lambda 表达式的实践。

先决条件

  • 熟悉函数类型和 lambda 表达式。
  • 熟悉尾随 lambda 语法,例如使用repeat()函数。
  • 了解 Kotlin 中的各种集合类型,例如List

您将学习的内容

  • 如何在字符串中嵌入 lambda 表达式。
  • 如何将各种高阶函数与List集合一起使用,包括forEach()map()filter()groupBy()fold()sortedBy()

您需要的内容

  • 可以访问 Kotlin Playground 的 Web 浏览器。

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()采用单个操作参数——类型为(T) -> Unit的函数。

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

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

  1. main()中,在cookies列表上调用forEach(),使用尾随 lambda 语法。因为尾随 lambda 是唯一的参数,所以调用函数时可以省略括号。
fun main() {
    cookies.forEach {
        
    }
}
  1. 在 lambda 主体中,添加一个println()语句,打印it
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: [email protected]
Menu item: [email protected]
Menu item: [email protected]
Menu item: [email protected]
Menu item: [email protected]
Menu item: [email protected]
Menu item: [email protected]

要访问属性并将其嵌入字符串中,您需要一个表达式。您可以通过用花括号括起来使表达式成为字符串模板的一部分。

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,则该项目不包含在新集合中。

如果您想在您的应用程序中获取数据的子集,这将非常有用。例如,假设面包店想要在菜单的单独部分突出显示其软烤饼干。您可以先filter()cookies列表,然后再打印项目。

  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. 创建一个名为 softBakedMenu 的变量,其中包含 groupedMenu[true] 的值,以及一个名为 crunchyMenu 的变量,其中包含 groupedMenu[false] 的值。因为对 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 表达式,该表达式返回你想要排序的属性。例如,如果你想按 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. 了解更多