Kotlin 高级集合功能
这篇博文是对我们YouTube 系列视频的配套文章,您可以在我们的Kotlin YouTube 频道上找到该视频,或者直接在这里观看!
今天,我们将学习一些高级函数,这些函数可以用来处理和操作各种 Kotlin 集合!
检查谓词:any,none和all
让我们先来看一些函数,这些函数可以让我们检查集合元素的条件,以此来热热身。
它们分别称为any、none和all。它们都接受一个谓词true(即返回或的函数false),并检查集合是否符合该谓词。
假设我们有一群朋友(实际上就是一个小组List<Person>,每个小组都包含一个成员name、一个朋友age,可能还有一个朋友driversLicense):
data class Person(val name: String, val age: Int, val driversLicense: Boolean = false)
val friendGroup = listOf(
Person("Jo", 19),
Person("Mic", 15),
Person("Hay", 33, true),
Person("Cal", 25)
)
当我们想检查该群体是否可以驾车出行时,我们需要检查他们当中是否有人持有驾照——所以我们使用该any函数。如果集合中至少true有一个元素满足谓词条件,则该函数返回 true 。true
val groupCanTravel = friendGroup.any { it.driversLicense }
// true
再举一个例子,假设我们想检查这群朋友是否可以进入俱乐部——为此,我们需要确保这群人中没有人未成年!
在这里,我们可以使用这个none函数,它只有在集合中没有任何元素满足我们的谓词条件true时才会返回:
val groupGetsInClub = friendGroup.none { it.age < 18 }
// false
第三个函数是 ` allfunction`。此时,你可能已经发现了其中的规律——如果集合中的每个元素都符合我们的谓词,all则返回 `true` true。我们可以用它来检查朋友群里的所有名字是否都很短:
val groupHasShortNames = friendGroup.all { it.name.length < 4 }
// true
空集合的谓词
既然说到这个话题,我们来做个小脑筋急转弯:对于空集合,any `a`、none`b` 和 `c`的行为是怎样的?all
val nobody = emptyList<Person>()
// what happens here?
我们先来看any第一个例子。没有元素可以满足谓词,所以它返回false:
nobody.any { it.driversLicense }
// false
同样的情况也适用于none——没有任何函数可以违反我们的谓词,所以它返回 true:
nobody.none { it.age < 18 }
// true
all然而,该函数返回的true是一个空集合。这乍一看可能会让你感到惊讶:
nobody.all { it.name.count() < 4 }
但这完全是有意为之且合情合理的:你不能指定一个违反谓词的元素。因此,谓词必须对集合中的所有元素都为真——即使集合中没有元素!
乍一看,这可能有点令人费解,但你会发现,这个被称为“空洞真理”的概念,实际上与检查条件和在程序代码中表达逻辑配合得非常好。
收藏品部分:chunked和windowed
我们的大脑刚刚被刺激了一下,让我们继续学习下一个主题,了解如何将收藏品拆分成多个部分!
该chunked函数
如果我们有一个只包含大量元素的集合,我们可以使用chunked函数将列表分割成特定大小的多个块。返回的结果是一个列表的列表,其中每个元素都是原始列表的一个_块_:
val objects = listOf("🌱", "🚀", "💡", "🐧", "⚙️", "🤖", "📚")
println(objects.chunked(3))
// [[🌱, 🚀, 💡], [🐧, ⚙️, 🤖], [📚]]
在上面的例子中,我们将随机对象列表(用表情符号表示)拆分,每次拆分大小为 3。
-
结果中的第一个元素本身就是一个列表,其中包含了我们的前三个对象——
[🌱, 🚀, 💡]。 -
第二个元素又是一个块,包含接下来的三个元素——
[🐧, ⚙️, 🤖]。 -
最后一个元素也是一个块——但是由于我们没有足够的元素来用三个项目填充它,所以它只包含书堆——
[📚]。
按照标准库的惯例,该chunked函数还提供了一些额外的功能。为了立即转换我们刚刚创建的数据块,我们可以应用一个转换函数。例如,我们可以反转结果列表中元素的顺序,而无需map单独调用其他函数:
println(objects.chunked(3) { it.reversed() })
// [[💡, 🚀, 🌱], [🤖, ⚙️, 🐧], [📚]]
总结起来:该chunked函数将我们的原始集合分割成列表的列表,其中每个列表的大小都是指定的大小。
该windowed函数
与此密切相关的是另一个windowed函数。它也返回我们集合中的列表的列表。然而,该函数并非将集合分割成多个部分,而是生成一个集合的“滑动窗口”:
println(objects.windowed(3))
// [[🌱, 🚀, 💡], [🚀, 💡, 🐧], [💡, 🐧, ⚙️], [🐧, ⚙️, 🤖], [⚙️, 🤖, 📚]]
- 第一个窗口再次包含前三个元素——
[🌱, 🚀, 💡]。 - 下一个窗口是
[🚀, 💡, 🐧]——我们只是将大小为 3 的窗口向右移动了一个单位,其中包含一些重叠部分。
该windowed函数还可以进行自定义。我们可以更改窗口大小和步长,步长是指窗口在每个步骤中“滑动”的元素数量:
println(objects.windowed(4, 2, partialWindows = true))
// [[🌱, 🚀, 💡, 🐧], [💡, 🐧, ⚙️, 🤖], [⚙️, 🤖, 📚], [📚]]
如上例所示,我们还可以控制结果是否包含部分窗口。这会改变我们在输入集合末尾、元素不足时的行为。
启用部分窗口后,我们只需不断滑动,即可将最后的元素以较小的窗口形式慢慢添加进来,直到我们得到一个窗口,该窗口再次只包含我们输入集合中的最后一个元素[⚙️, 🤖, 📚], [📚]。
windowed此外,它还允许我们在最后执行额外的转换,可以直接修改各个窗口:
println(objects.windowed(4, 2, true) {
it.reversed()
})
// [[🐧, 💡, 🚀, 🌱], [🤖, ⚙️, 🐧, 💡], [📚, 🤖, ⚙️], [📚]]
取消嵌套集合:扁平化和扁平化映射
` chunkedand`windowed函数以及其他一些函数都会返回嵌套集合——列表的列表。如果我们想解嵌套,将它们转换回扁平的元素列表该怎么办?别担心,标准库已经帮我们解决了这个问题。
我们可以对flatten一个嵌套集合调用该函数。正如你可能猜到的那样,结果是一个包含所有嵌套集合中原始元素的列表:
val objects = listOf("🌱", "🚀", "💡", "🐧", "⚙️", "🤖", "📚")
objects.windowed(4, 2, true) {
it.reversed()
}.flatten()
// [🐧, 💡, 🚀, 🌱, 🤖, ⚙️, 🐧, 💡, 📚, 🤖, ⚙️, 📚]
这里也正好可以谈谈这个flatMap函数。它就像是先使用 `getElementById` ,然后再使用`getElementById`flatMap的组合——它接受一个 lambda 表达式,该表达式会从输入集合中的每个元素生成一个集合:mapflatten
val lettersInNames = listOf("Lou", "Mel", "Cyn").flatMap {
it.toList()
}
println(lettersInNames)
// [L, o, u, M, e, l, C, y, n]
在上面的例子中,我们提供的函数会为输入集合中的每个元素创建一个列表,其中包含原始字符串的字母。接下来,这个集合的集合会被扁平化。正如我们所期望的,最终我们得到一个纯元素列表——即原始集合名称中的字符列表。
如果您正在对列表进行操作,该操作会为每个输入元素生成一个集合,请考虑它是否flatMap可以帮助您简化代码!
合并收藏集:zip和unzip
到目前为止,我们一直都在研究单个集合及其功能。现在让我们来学习如何合并两个集合,并从中创建一个新集合——是时候了zip!
该zip函数
假设我们有两个集合,其中每个索引处的元素都以某种方式相关。例如,一个集合可以是德国城市列表,另一个集合可以是与这些城市对应的德国车牌列表:
val germanCities = listOf(
"Aachen",
"Bielefeld",
"München"
)
val germanLicensePlates = listOf(
"AC",
"BI",
"M"
)
println(germanCities.zip(germanLicensePlates))
// [(Aachen, AC), (Bielefeld, BI), (München, M)]
如您所见,通过将这两个集合压缩,我们得到一个对的列表,其中每个对包含原始两个集合中具有相同索引的元素。
打个比方,这就像夹克上的拉链,齿一个接一个地对接。我们将收藏品的各个元素“拉链”连接起来,最终得到每座城市及其对应车牌的配对。
为了增添一些特色,我们还可以zip使用中缀表达式来调用该函数:
println(germanCities zip germanLicensePlates)
// [(Aachen, AC), (Bielefeld, BI), (München, M)]
zip也可以接受转换函数。我们可以传递一个 lambda 函数,该函数接收各个压缩对的值,然后我们可以应用转换:
println(germanCities.zip(germanLicensePlates) { city, plate ->
city.uppercase() to plate.lowercase()
})
// [(AACHEN, ac), (BIELEFELD, bi), (MÜNCHEN, m)]
该unzip函数
标准库还包含一个名为 `inverse` 的逆函数,unzip它接受一个键值对列表,并将它们拆分回两个单独的列表:
val citiesToPlates = germanCities.zip(germanLicensePlates) { city, plate ->
city.uppercase() to plate.lowercase()
}
val (cities, plates) = citiesToPlates.unzip()
println(cities)
// [AACHEN, BIELEFELD, MÜNCHEN]
println(plates)
// [ac, bi, m]
上面的示例使用解构声明来轻松访问这两个对象。
该zipWithNext函数
从某种意义上说,zipWithNext这实际上是我们今天所了解的函数的一个特殊情况windowed:这个函数不是将两个单独的列表逐个元素地压缩在一起,而是接受一个集合,并将其中的每个元素与其后面的元素压缩在一起:
val random = listOf(3, 1, 4, 1, 5, 9, 2, 6, 5, 4)
println(random.zipWithNext())
// [(3, 1), (1, 4), (4, 1), (1, 5), (5, 9), (9, 2), (2, 6), (6, 5), (5, 4)]
在上面的例子中,我们将一系列数字压缩在一起。如果我们想检查“变化量”(即每一步数值的增减量),我们可以用 `zip` 函数非常优雅地表达出来zipWithNext。我们提供一个 lambda 表达式,它接收一个包含两个数字的对,第一个数字和紧随其后的数字:
val random = listOf(3, 1, 4, 1, 5, 9, 2, 6, 5, 4)
val changes = random.zipWithNext { a, b -> b - a }
println(changes)
// [-2, 3, -3, 4, 4, -7, 4, -1, -1]
自定义聚合:reduce和fold
我们终于来到了本文的压轴大戏——帮助我们构建自定义聚合的函数。
该reduce函数
让我们先通过一个简单的回调函数来引入主题——在上一篇文章中,我们学习了诸如sum` average__min__`、` count__max__` 和 `__max__` 之类的函数,以及用于接收集合中最小和最大元素的函数。所有这些函数都会将我们的集合简化为一个单一值。
有时我们会遇到这样的情况:没有现成的函数可以满足我们为集合生成单个值的需求。例如,我们可能想要将列表中的所有数字相乘,而不是对它们求和。
在这种情况下,我们可以将该reduce函数作为更通用的聚合集合的版本:
val random = listOf(3, 1, 4, 1, 5, 9, 2, 6, 5, 4)
val multiplicativeAggregate = random.reduce { acc, value -> acc * value }
println(multiplicativeAggregate)
// 129600
如上例所示,我们使用 lambda 代码块调用 reduce 函数,该 lambda 代码块接收两个参数:
- 一个累加器,其类型与我们的集合相同,并且
- 我们馆藏中的一件单件藏品。
lambda 函数的任务是返回一个新的累加器。每次调用,依次进行,不仅接收当前元素,还接收累加器内部先前计算的结果。
- 该函数从累加器中集合的第一个元素开始。
- 然后它运行我们的操作——在本例中,我们将累加器(现在是第一个数字)与当前元素(第二个数字)相乘。
- 我们已经计算出一个新值,该值将存储在累加器中,并在下次使用第三个元素调用函数时使用。
这个循环不断重复,我们持续地在累加器中逐步积累最终结果。甚至可以说,我们正在积累这个结果!
遍历完集合中的所有元素后,reduce返回累加器中的最终值。
如您所见,通过这种方式reduce,我们可以将聚合集合的许多机制隐藏在一个函数调用之后,并保持 Kotlin 简洁的特性。
该fold函数
但我们其实可以更进一步,利用这种多功能性——通过fold运算。记住,当我们使用时reduce,迭代从累加器中输入集合的第一个元素开始。
通过这个fold函数,我们可以指定自己的累加器——实际上,它的类型甚至可以与输入集合中的元素类型不同!例如,我们可以接受一个单词列表,并使用以下公式将它们的字符数相乘fold:
val fruits = listOf("apple", "cherry", "banana", "orange")
val multiplied = fruits.fold(1) { acc, value ->
acc * value.length
}
println(multiplied) // 1080
其底层机制相同——传递给fold函数的 lambda 表达式会接收一个累加器和一个值作为参数,并计算一个新的累加器。区别在于,我们可以自行指定累加器的初始值。
(注意,我们传递的1是累加器的初始值,而不是 1。0这是因为在乘法运算中,1 是单位元。)
两者fold都有reduce多种其他口味可供选择:
– 同级函数reduceRight并foldRight改变迭代顺序
reduceOrNull允许您处理空集合而不会抛出异常。runningFold而且,runningReduce它不仅返回表示累加器最终状态的单个值,而且还返回累加器所有中间值的列表。
就是这样!
以上就是我对 Kotlin 中一些高级集合操作的概述——希望这篇文章对您有所帮助,并让您学到了一些新知识!
也许你可以在代码中找到一些地方,用谓词、压缩、分块或窗口化等方法会很有帮助!或者,你也可以探索一下,基于这些reduce函数定义自己的聚合函数fold。
要及时收到 Kotlin 新内容的发布提醒,请关注我们的dev.to/kotlin页面,并务必在 Twitter 上关注我@sebi_io。
另外,请务必利用这个机会,在我们的YouTube 频道上找到订阅按钮和通知铃!
小心!
文章来源:https://dev.to/kotlin/advanced-kotlin-collection-functionality-5e90

