发布于 2026-01-06 1 阅读
0

Kotlin 高级集合功能

Kotlin 高级集合功能

这篇博文是对我们YouTube 系列视频的配套文章,您可以在我们的Kotlin YouTube 频道上找到该视频,或者直接在这里观看

今天,我们将学习一些高级函数,这些函数可以用来处理和操作各种 Kotlin 集合!

检查谓词:anynoneall

让我们先来看一些函数,这些函数可以让我们检查集合元素的条件,以此来热热身。

它们分别称为anynoneall。它们都接受一个谓词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)
)
Enter fullscreen mode Exit fullscreen mode

当我们想检查该群体是否可以驾车出行时,我们需要检查他们当中是否有人持有驾照——所以我们使用该any函数。如果集合中至少true有一个元素满足谓词条件,则该函数返回 true true

val groupCanTravel = friendGroup.any { it.driversLicense }
// true
Enter fullscreen mode Exit fullscreen mode

再举一个例子,假设我们想检查这群朋友是否可以进入俱乐部——为此,我们需要确保这群人中没有人未成年!

在这里,我们可以使用这个none函数,它只有在集合中没有任何元素满足我们的谓词条件true时才会返回:

val groupGetsInClub = friendGroup.none { it.age < 18 }
// false
Enter fullscreen mode Exit fullscreen mode

第三个函数是 ` allfunction`。此时,你可能已经发现了其中的规律——如果集合中的每个元素都符合我们的谓词,all则返回 `true` true。我们可以用它来检查朋友群里的所有名字是否都很短:

val groupHasShortNames = friendGroup.all { it.name.length < 4 }
// true
Enter fullscreen mode Exit fullscreen mode

空集合的谓词

既然说到这个话题,我们来做个小脑筋急转弯:对于空集合,any `a`、none`b` 和 `c`的行为是怎样的all

val nobody = emptyList<Person>()
// what happens here?
Enter fullscreen mode Exit fullscreen mode

我们先来看any第一个例子。没有元素可以满足谓词,所以它返回false

nobody.any { it.driversLicense }
// false
Enter fullscreen mode Exit fullscreen mode

同样的情况也适用于none——没有任何函数可以违反我们的谓词,所以它返回 true:

nobody.none { it.age < 18 }
// true
Enter fullscreen mode Exit fullscreen mode

all然而,该函数返回的true是一个空集合。这乍一看可能会让你感到惊讶:

nobody.all { it.name.count() < 4 } 
Enter fullscreen mode Exit fullscreen mode

但这完全是有意为之且合情合理的:你不能指定一个违反谓词的元素。因此,谓词必须对集合中的所有元素都为真——即使集合中没有元素

乍一看,这可能有点令人费解,但你会发现,这个被称为“空洞真理”的概念,实际上与检查条件和在程序代码中表达逻辑配合得非常好。

收藏品部分:chunkedwindowed

我们的大脑刚刚被刺激了一下,让我们继续学习下一个主题,了解如何将收藏品拆分成多个部分!

chunked函数

如果我们有一个只包含大量元素的集合,我们可以使用chunked函数将列表分割成特定大小的多个块。返回的结果是一个列表的列表,其中每个元素都是原始列表的一个_块_:

val objects = listOf("🌱", "🚀", "💡", "🐧", "⚙️", "🤖", "📚")
println(objects.chunked(3))
// [[🌱, 🚀, 💡], [🐧, ⚙️, 🤖], [📚]]
Enter fullscreen mode Exit fullscreen mode

在上面的例子中,我们将随机对象列表(用表情符号表示)拆分,每次拆分大小为 3。

  • 结果中的第一个元素本身就是一个列表,其中包含了我们的前三个对象—— [🌱, 🚀, 💡]

  • 第二个元素又是一个块,包含接下来的三个元素—— [🐧, ⚙️, 🤖]

  • 最后一个元素也是一个块——但是由于我们没有足够的元素来用三个项目填充它,所以它只包含书堆—— [📚]

按照标准库的惯例,该chunked函数还提供了一些额外的功能。为了立即转换我们刚刚创建的数据块,我们可以应用一个转换函数。例如,我们可以反转结果列表中元素的顺序,而无需map单独调用其他函数:

println(objects.chunked(3) { it.reversed() })
// [[💡, 🚀, 🌱], [🤖, ⚙️, 🐧], [📚]]
Enter fullscreen mode Exit fullscreen mode

总结起来:该chunked函数将我们的原始集合分割成列表的列表,其中每个列表的大小都是指定的大小。

windowed函数

与此密切相关的是另一个windowed函数。它也返回我们集合中的列表的列表。然而,该函数并非将集合分割成多个部分,而是生成一个集合的“滑动窗口”:

println(objects.windowed(3))
// [[🌱, 🚀, 💡], [🚀, 💡, 🐧], [💡, 🐧, ⚙️], [🐧, ⚙️, 🤖], [⚙️, 🤖, 📚]]
Enter fullscreen mode Exit fullscreen mode
  • 第一个窗口再次包含前三个元素—— [🌱, 🚀, 💡]
  • 下一个窗口是[🚀, 💡, 🐧]——我们只是将大小为 3 的窗口向右移动了一个单位,其中包含一些重叠部分。

windowed函数还可以进行自定义。我们可以更改窗口大小和长,步长是指窗口在每个步骤中“滑动”的元素数量:

println(objects.windowed(4, 2, partialWindows = true))
// [[🌱, 🚀, 💡, 🐧], [💡, 🐧, ⚙️, 🤖], [⚙️, 🤖, 📚], [📚]]
Enter fullscreen mode Exit fullscreen mode

如上例所示,我们还可以控制结果是否包含部分窗口。这会改变我们在输入集合末尾、元素不足时的行为。

启用部分窗口后,我们只需不断滑动,即可将最后的元素以较小的窗口形式慢慢添加进来,直到我们得到一个窗口,该窗口再次只包含我们输入集合中的最后一个元素[⚙️, 🤖, 📚], [📚]

windowed此外,它还允许我们在最后执行额外的转换,可以直接修改各个窗口:

println(objects.windowed(4, 2, true) {
    it.reversed()
})
// [[🐧, 💡, 🚀, 🌱], [🤖, ⚙️, 🐧, 💡], [📚, 🤖, ⚙️], [📚]]
Enter fullscreen mode Exit fullscreen mode

取消嵌套集合:扁平化和扁平化映射

` chunkedand`windowed函数以及其他一些函数都会返回嵌套集合——列表的列表。如果我们想嵌套,将它们转换回扁平的元素列表该怎么办?别担心,标准库已经帮我们解决了这个问题。

我们可以对flatten一个嵌套集合调用该函数。正如你可能猜到的那样,结果是一个包含所有嵌套集合中原始元素的列表:

val objects = listOf("🌱", "🚀", "💡", "🐧", "⚙️", "🤖", "📚")
objects.windowed(4, 2, true) {
    it.reversed()
}.flatten()
// [🐧, 💡, 🚀, 🌱, 🤖, ⚙️, 🐧, 💡, 📚, 🤖, ⚙️, 📚]
Enter fullscreen mode Exit fullscreen mode

这里也正好可以谈谈这个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]
Enter fullscreen mode Exit fullscreen mode

在上面的例子中,我们提供的函数会为输入集合中的每个元素创建一个列表,其中包含原始字符串的字母。接下来,这个集合的集合会被扁平化。正如我们所期望的,最终我们得到一个纯元素列表——即原始集合名称中的字符列表。

如果您正在对列表进行操作,该操作会为每个输入元素生成一个集合,请考虑它是否flatMap可以帮助您简化代码!

合并收藏集:zipunzip

到目前为止,我们一直都在研究单个集合及其功能。现在让我们来学习如何合并两个集合,并从中创建一个新集合——是时候了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)]
Enter fullscreen mode Exit fullscreen mode

如您所见,通过将这两个集合压缩,我们得到一个对的列表,其中每个对包含原始两个集合中具有相同索引的元素。

打个比方,这就像夹克上的拉链,齿一个接一个地对接。我们将收藏品的各个元素“拉链”连接起来,最终得到每座城市及其对应车牌的配对。

为了增添一些特色,我们还可以zip使用中缀表达式来调用该函数:

println(germanCities zip germanLicensePlates)
// [(Aachen, AC), (Bielefeld, BI), (München, M)]
Enter fullscreen mode Exit fullscreen mode

zip也可以接受转换函数。我们可以传递一个 lambda 函数,该函数接收各个压缩对的值,然后我们可以应用转换:

println(germanCities.zip(germanLicensePlates) { city, plate ->
    city.uppercase() to plate.lowercase()
})
// [(AACHEN, ac), (BIELEFELD, bi), (MÜNCHEN, m)]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

上面的示例使用解构声明来轻松访问这两个对象。

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)]
Enter fullscreen mode Exit fullscreen mode

在上面的例子中,我们将一系列数字压缩在一起。如果我们想检查“变化量”(即每一步数值的增减量),我们可以用 `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]
Enter fullscreen mode Exit fullscreen mode

自定义聚合:reducefold

我们终于来到了本文的压轴大戏——帮助我们构建自定义聚合的函数。

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
Enter fullscreen mode Exit fullscreen mode

如上例所示,我们使用 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
Enter fullscreen mode Exit fullscreen mode

其底层机制相同——传递给fold函数的 lambda 表达式会接收一个累加器和一个值作为参数,并计算一个新的累加器。区别在于,我们可以自行指定累加器的初始值。

注意,我们传递的1是累加器的初始值,而不是 1。0这是因为在乘法运算中,1 是单位元。)

两者fold都有reduce多种其他口味可供选择:

– 同级函数reduceRightfoldRight改变迭代顺序

  • reduceOrNull允许您处理空集合而不会抛出异常。
  • runningFold而且,runningReduce它不仅返回表示累加器最终状态的单个值,而且还返回累加器所有中间值的列表。

就是这样!

以上就是我对 Kotlin 中一些高级集合操作的概述——希望这篇文章对您有所帮助,并让您学到了一些新知识!

也许你可以在代码中找到一些地方,用谓词、压缩、分块或窗口化等方法会很有帮助!或者,你也可以探索一下,基于这些reduce函数定义自己的聚合函数fold

要及时收到 Kotlin 新内容的发布提醒,请关注我们的dev.to/kotlin页面,并务必在 Twitter 上关注我@sebi_io

另外,请务必利用这个机会,在我们的YouTube 频道上找到订阅按钮和通知铃

小心!

文章来源:https://dev.to/kotlin/advanced-kotlin-collection-functionality-5e90