当你操作集合时,函数式风格提供了许多好处。你可以将库函数用于大部分任务并简化你的代码。在这一章节,我们将会讨论一些用于集合的Kotlin标准库函数。我们主要以filter和map,以及它们背后的概念为开始。我们也会覆盖其他有用的函数。我们会给你一些有关如何重用它们,以及如何编写更加清晰易读的代码的提示。
注意,其中没有一个函数是Kotlin的设计者发明的。这些或类似的函数对于所有支持lambda的语言都是可用的。如果你以及对这些概念非常熟悉了,你可以通过下面的例子快速看一眼并跳过其中的解释。

5.2.1 必要的:filter和map

filtermap函数组成了操作集合的基础。许多的集合请求可以在它们的帮助下进行表达。
对每一个函数,我们都将提供一个有数字和属性的Person类的例子:

data class Person(val name: String, val age: Int)

filter函数变换一个集合,并过滤出不满足给定断言的元素:

>>> val list = listOf(1, 2, 3, 4)
>>> list.filter { it % 2 == 0 } // 1 只保留偶数
[2, 4]

结果是只包含来自输入集合满足断言的元素的新集合。如图5.3所示:

图5.3

图5.3 过滤器是如何工作的

如果你只想保留年龄大于30的人,你可以使用filter:

>>> val people = listOf(Person("Alice", 29), Person("Bob", 31))
>>> people.filter { it.age > 30 }
[Person("Bob", 31)]

filter函数可以从集合中移除不想要的元素。但是它不能修改元素。要修改元素,map就起作用了。
map函数对集合中的每个元素应用给定的函数,并且收集结果形成新的集合。例如,你可以把一列数字变换为它们的平方:

>>> val list = listOf(1, 2, 3, 4)
>>> list.map { it * it }
[1, 4, 9, 16]

结果是一个包含同样的元素个数,但是每个元素根据给定的断言进行变换的集合(详见图5.4)。

图5.4

图5.4 map函数的工作原理

如果你只是想打印一列名字,而不是一列人,你可以使用map来变换列表:

>>> val people = listOf(Person("Alice", 29), Person("Bob", 31))
>>> people.map { it.name }
[Alice, Bob]

注意了,这个例子可以使用成员引用完美的重写:

people.map(Person::name)

你可以方便的将多个调用像这样串成一条链子。例如,我们打印年龄大于30的人的名字:

>>> people.filter { it.age > 30 }.map(Person::name)
[Bob]

现在,我们假设你需要组中最年长的人的名字。你可以找出组中年纪最大的人。然后返回每一个人的年龄。这样的代码用lambda写非常简单:

people.filter { it.age == people.maxBy(Person::age).age }

但是要注意,这份代码为每个人重复了最大年龄的查找步骤。所以,如果集合中有100个人,最大年龄的搜索将会执行100次!
下面的解决方案进行了优化。它只计算一次最大年龄:

val maxAge = people.maxBy(Person::age).age
people.filter { it.age == maxAge }

如果你没有需要,就不要重复进行计算。看上去简单的代码,使用lambda表达式,有时候可以隐藏底层操作的复杂性。所以,你总能记住你写的代码中发生了什么。

你也可以对映射应用过滤和变换函数:

>>> val numbers = mapOf(0 to "zero", 1 to "one")
>>> println(numbers.mapValues { it.value.toUpperCase() })
{0=ZERO, 1=ONE}

Kotlin有函数来分别操作键和值。filterKeysmapKeys过滤并变换映射的键。相应的,filterValuesmapValues过滤和变换对应的值。

5.2.2 all, any, countfind:对集合应用预言

另一个常见的任务是检查集合中所有的元素是否匹配某个条件。Kotlin中,这可以通过allany函数进行表达。count函数检查多少个元素满足这个预言。find函数返回第一个匹配的元素。
为了介绍这些函数,我们定义一个语言canBeInClub27来检查某个人是否为27岁或者更年轻:

val canBeInClub27 = { p: Person -> p.age <= 27 }

如果你对所有的元素是否符合这个预言,你可以使用all函数:

>>> val people = listOf(Person("Alice", 27), Person("Bob", 31))
>>> println(people.all(canBeInClub27))
false

如果你需要检查是否至少有一个匹配的元素,使用any

>>> println(people.any(canBeInClub27))
true

要注意,!all(非全部)可以使用一个否定条件来替代any,反之亦然。为了让你对代码更加容易理解,你最好选择一个不需要你在他都前面放置否定符号的函数:

>>> val list = listOf(1, 2, 3)
>>> println(!list.all { it == 3 }) // 1 否定符号!并不引人注意。所以,这个案例中使用"any"更加合适
true
>>> println(list.any { it != 3 }) // 2 参数中的条件改成了相反的情况
true

第一个检查确保并非所有元素都等于3。它等价于至少有一个不等于3的元素。这就是你在第二行使用any进行的检查。
如果你想知道有多少个元素符合这个预言,使用count:

>>> val people = listOf(Person("Alice", 27), Person("Bob", 31))
>>> println(people.count(canBeInClub27))
1

SIDEBAR 为任务使用右边的函数:count vs size
你很容易忘记了count的存在。而且你通过过滤集合实现它的功能,得到了它的大小:

>>> println(people.filter(canBeInClub27).size)
1

在这个案例中,创建了一个临时的集合来保存所有符合预言的元素。另一方面,count方法只追踪匹配的元素的个数,而不是元素本身。因此,它会更加高效。
作为一个通用的法则,试着去找出满足你的需求的最合适的操作。

为了找出一个符合预言的元素,请使用find函数:

>>> val people = listOf(Person("Alice", 27), Person("Bob", 31))
>>> println(people.find(canBeInClub27))
Person(name=Alice, age=27)

如果有多个元素,函数将返回第一个匹配的元素。如果没有满足的元素,函数返回nullfind的一个同义词是firstOrNull。如果如能够更加清晰的表达你对想法,你可以使用firstOrNull函数。

5.2.3 groupBy:把一个列表转换为多组映射

想象一下,你需要根据你某些特性来将所有元素分割成不同的组。例如,你想把年龄相同的人放在一组。把这个特性直接作为一个参数进行传递非常方便!groupBy函数可以为你做到这一点:

>>> val people = listOf(Person("Alice", 31),
...
Person("Bob", 29), Person("Carol", 31))
>>> println(people.groupBy { it.age })

这个操作的结果是一个映射通过键被分成多个组。见图5.5。

图5.5

图5.5 应用groupBy函数的结果

对于这个案例,输出以下结果:

{29=[Person(name=Bob, age=29)],
31=[Person(name=Alice, age=31), Person(name=Carol, age=31)]}

每一组都被保存成一个列表。所以结果的类型为Map<Int, List<Person>>。你可以使用像mapKeysmapValues这样的函数对这个映射做更多的修改。

正如另一个例子所示,我们来看看如何使用成员引用通过第一个字符对字符串进行分组:

>>> val list = listOf("a", "ab", "b")
>>> println(list.groupBy(String::first))
{a=[a, ab], b=[b]}

注意,这里的first不是String类的成员。它是一个扩展(函数)。不过你可以把它当做一个成员引用进行访问。

5.2.4 flatMap和flatten:处理嵌套集合中的元素

现在,让我们把有关人的讨论放到一边,切换到书的讨论。假定你有一个书库,用Book类来表示:

class Book(val title: String, val authors: List<String>)

每一本书有一个或多个作者。你可以计算你的书库中所有作者的集合:

books.flatMap { it.authors }.toSet() // 编写书籍的所有作者的集合

flatMap函数做了两件事:首先它根据作为参数而给定的函数把每一个元素都变换(或映射)到一个集合中。然后它把多个列表合并为一个。有一个处理字符串的案例很好的解析了这个概念(见图5.6):

>>> val strings = listOf("abc", "def")
>>> println(strings.flatMap { it.toList() })
[a, b, c, d, e, f]

图5.6 应用flatMap函数后的结果

字符串的toList()函数将其自身转换为字符的列表。如果你有把map函数跟toList()一起使用,如图中的第二行所示,你会得到一个嵌套在列表中的列表的字符串。flatMap函数也做了以下步骤。之后返回一个由所有元素组成的列表。
我们来看看返回的作者:

>>> val books = listOf(Book("Thursday Next", listOf("Jasper Fforde")),
Book("Mort", listOf("Terry Pratchett")),
... Book("Good Omens", listOf("Terry Pratchett",
... "Neal Gaiman")))
>>> println(books.flatMap { it.authors }.toSet())
[Jasper Fforde, Terry Pratchett, Neal Gaiman]

每一本书都由一个或多个作者编写。book.authors属性保存了作者的集合。flatMap函数把所有书的作者都组装进了一个扁平的(没有嵌套的)列表。toSet调用从结果集合中移除了重复的元素。所以,在这个案例中,Terry Pratchett只会在打印输出中列举一次。
 当你陷入必须合并为一个集合的嵌套集合的元素时,你可以考虑flatMap。注意,如果你需要变换任何东西,只是仅仅需要把这样一个集合变得扁平,你可以使用flatten函数:listOfLists.flatten()
 我们重点讲了Kotlin标准库中的一些集合操作函数。但是里边还有更多的函数。处于篇幅的考虑,也因为展示一长串的函数非常的无聊,我们不会面面俱到。我们一般建议,当你编写用到了集合的代码时,考虑如何将操作表达为一个通用的变换。然后查找执行这个变换的库函数。你很可能找到一个这样的函数,并用它来解决你的问题。这往往比你手工实现更加快速。
 现在,我们来仔细看看集合的链式操作吧!在下一章节,你将会看到操作执行的几种不同方式。

results matching ""

    No results matching ""