9.1.1.6 集合转换操作

原文链接: https://kotlinlang.org/docs/collection-transformations.html

9.1.1.6 集合转换操作

Kotlin 标准库提供了一组用于集合转换的扩展函数。这些函数根据所提供的转换规则,从现有集合构建新集合。本页将概述可用的集合转换函数。

Map

映射转换会基于对另一个集合的元素应用函数所得的结果创建集合。基本的映射函数是 map()。它把给定的 lambda 函数应用于每个元素并返回由 lambda 结果组成的列表。结果的顺序与元素的原始顺序相同。如果要应用一个额外使用元素索引作为参数的转换,请使用 mapIndexed()。

1
2
3
4
5
6

fun main() {
    val numbers = setOf(1, 2, 3)
    println(numbers.map { it * 3 })
    println(numbers.mapIndexed { idx, value -> value * idx })
}

如果转换对某些元素产生 null,你可以改为调用 mapNotNull() 而不是 map(),或调用 mapIndexedNotNull() 而不是 mapIndexed(),从而把 null 从结果集合中过滤掉。

1
2
3
4
5
6

fun main() {
    val numbers = setOf(1, 2, 3)
    println(numbers.mapNotNull { if ( it == 2) null else it * 3 })
    println(numbers.mapIndexedNotNull { idx, value -> if (idx == 0) null else value * idx })
}

转换映射时有两种选择:转换键而保持值不变,或者反过来。要对键应用给定的转换,请使用 mapKeys();相应地,mapValues() 用于转换值。这两个函数使用的转换都以映射条目作为参数,因此你可以同时操作它的键和值。

1
2
3
4
5
6

fun main() {
    val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key11" to 11)
    println(numbersMap.mapKeys { it.key.uppercase() })
    println(numbersMap.mapValues { it.value + it.key.length })
}

Zip

*压缩(Zipping)*转换是根据两个集合中位置相同的元素构建键值对。在 Kotlin 标准库中,这由 zip() 扩展函数完成。

当以一个集合或数组作为参数,在另一个集合或数组上调用时,zip() 会返回由 Pair 对象组成的 List。接收者集合中的元素是这些键值对中的第一个元素。

如果两个集合大小不同,zip() 的结果以较小的那个大小为准;较大集合末尾的元素不会包含在结果中。

zip() 也可以用中缀形式 a zip b 调用。

1
2
3
4
5
6
7
8
9

fun main() {
    val colors = listOf("red", "brown", "grey")
    val animals = listOf("fox", "bear", "wolf")
    println(colors zip animals)

    val twoAnimals = listOf("fox", "bear")
    println(colors.zip(twoAnimals))
}

你还可以用一个接收两个参数的转换函数调用 zip():接收者元素和参数元素。在这种情况下,结果 List 包含对位置相同的接收者元素与参数元素组成的键值对调用该转换函数所得的返回值。

1
2
3
4
5
6
7

fun main() {
    val colors = listOf("red", "brown", "grey")
    val animals = listOf("fox", "bear", "wolf")

    println(colors.zip(animals) { color, animal -> "The ${animal.replaceFirstChar { it.uppercase() }} is $color"})
}

当你有一个由 Pair 组成的 List 时,你可以进行反向转换——解压(unzipping)——从这些键值对构建两个列表:

  • 第一个列表包含原始列表中每个 Pair 的第一个元素。
  • 第二个列表包含第二个元素。

要解压由键值对组成的列表,请调用 unzip()。

1
2
3
4
5

fun main() {
    val numberPairs = listOf("one" to 1, "two" to 2, "three" to 3, "four" to 4)
    println(numberPairs.unzip())
}

Associate

*关联(Association)*转换允许根据集合元素与和它们关联的某些值构建映射。在不同的关联类型中,元素可以是关联映射中的键或值。

基本的关联函数 associateWith() 创建的 Map 以原始集合的元素为键,值则由给定的转换函数根据这些元素产生。如果两个元素相等,映射中只保留最后一个。

1
2
3
4
5

fun main() {
    val numbers = listOf("one", "two", "three", "four")
    println(numbers.associateWith { it.length })
}

要构建以集合元素为值的映射,可以使用函数 associateBy()。它接收一个根据元素的值返回键的函数。如果两个元素的键相等,映射中只保留最后一个。

associateBy() 也可以带一个值转换函数来调用。

1
2
3
4
5
6
7

fun main() {
    val numbers = listOf("one", "two", "three", "four")

    println(numbers.associateBy { it.first().uppercaseChar() })
    println(numbers.associateBy(keySelector = { it.first().uppercaseChar() }, valueTransform = { it.length }))
}

构建键和值都以某种方式由集合元素产生的映射的另一种方式,是使用函数 associate()。它接收一个返回 Pair 的 lambda 函数:即相应映射条目的键和值。

请注意,associate() 会产生短生命周期的 Pair 对象,这可能影响性能。因此,只有在性能不关键、或者相比其他选项更可取时才应使用 associate()。

后一种情况的例子是,键和相应的值同时由某个元素产生。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14

fun main() {
data class FullName (val firstName: String, val lastName: String)

fun parseFullName(fullName: String): FullName {
    val nameParts = fullName.split(" ")
    if (nameParts.size == 2) {
        return FullName(nameParts[0], nameParts[1])
    } else throw Exception("Wrong name format")
}

    val names = listOf("Alice Adams", "Brian Brown", "Clara Campbell")
    println(names.associate { name -> parseFullName(name).let { it.lastName to it.firstName } })
}

这里我们先对元素调用一个转换函数,然后用该函数结果的属性构建一个键值对。

Flatten

如果你处理的是嵌套集合,可能会发现标准库中那些提供对嵌套集合元素进行扁平访问的函数很有用。

第一个函数是 flatten()。你可以在集合的集合上调用它,例如由 Set 组成的 List。该函数返回一个包含嵌套集合所有元素的 List。

1
2
3
4
5

fun main() {
    val numberSets = listOf(setOf(1, 2, 3), setOf(4, 5, 6), setOf(1, 2))
    println(numberSets.flatten())
}

另一个函数——flatMap()——提供了一种灵活处理嵌套集合的方式。它接收一个把集合元素映射到另一个集合的函数。因此,flatMap() 会返回对所有元素应用该函数所得结果组成的一个列表。所以,flatMap() 的行为相当于先调用 map()(以集合作为映射结果),然后调用 flatten()。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

data class StringContainer(val values: List<String>)

fun main() {
    val containers = listOf(
        StringContainer(listOf("one", "two", "three")),
        StringContainer(listOf("four", "five", "six")),
        StringContainer(listOf("seven", "eight"))
    )
    println(containers.flatMap { it.values })
}

字符串表示

如果你需要以可读的格式获取集合内容,请使用把集合转换为字符串的函数:joinToString() 和 joinTo()。

joinToString() 根据提供的参数,从集合元素构建单个 String。joinTo() 做同样的事,但会把结果追加到给定的 Appendable 对象中。

当使用参数的默认值调用时,这些函数返回的结果类似于对集合调用 toString():一个由元素字符串表示组成的 String,以逗号加空格分隔。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

fun main() {
    val numbers = listOf("one", "two", "three", "four")

    println(numbers)
    println(numbers.joinToString())

    val listString = StringBuffer("The list of numbers: ")
    numbers.joinTo(listString)
    println(listString)
}

要构建自定义的字符串表示,你可以在函数参数中指定 separator、prefix 和 postfix。结果字符串将以 prefix 开头、以 postfix 结尾。separator 会出现在除最后一个元素之外的每个元素之后。

1
2
3
4
5

fun main() {
    val numbers = listOf("one", "two", "three", "four")
    println(numbers.joinToString(separator = " | ", prefix = "start: ", postfix = ": end"))
}

对于较大的集合,你可能想指定 limit——即包含到结果中的元素数量。如果集合大小超过 limit,其他所有元素都会被替换为 truncated 参数对应的单个值。

1
2
3
4
5

fun main() {
    val numbers = (1..100).toList()
    println(numbers.joinToString(limit = 10, truncated = "<...>"))
}

最后,要自定义元素本身的表示方式,请提供 transform 函数。

1
2
3
4
5

fun main() {
    val numbers = listOf("one", "two", "three", "four")
    println(numbers.joinToString { "Element: ${it.uppercase()}"})
}