5.14 解构声明

原文链接: https://kotlinlang.org/docs/destructuring-declarations.html

5.14 解构声明

有时把一个对象解构为若干变量会很方便,例如:

1
val (name, age) = person

这种语法称为解构声明。解构声明会一次性创建多个变量。这里声明了两个新变量 name 和 age,你可以独立使用它们:

1
2
println(name)
println(age)

解构声明会被编译为如下代码:

1
2
val name = person.component1()
val age = person.component2()

component1() 和 component2() 函数是 Kotlin 中广泛使用的约定原则的又一个例子(参见 + 和 * 等运算符,for 循环也是例子)。解构声明的右侧可以是任何东西,只要能在它上面调用所需数量的组件函数。当然,也可以有 component3()、component4() 等等。

注意: componentN() 函数必须用 operator 关键字标记,才能在解构声明中使用。

解构声明在 for 循环中同样可用:

1
for ((a, b) in collection) { ... }

变量 a 和 b 会取得对集合元素调用 component1() 和 component2() 所返回的值。

示例:从函数返回两个值

假设你需要从函数返回两样东西——例如一个结果对象和某种状态。在 Kotlin 中,一种紧凑的做法是声明一个数据类并返回它的实例:

1
2
3
4
5
6
7
8
9
data class Result(val result: Int, val status: Status)
fun function(...): Result {
    // 计算

    return Result(result, status)
}

// 现在,使用这个函数:
val (result, status) = function(...)

由于数据类会自动声明 componentN() 函数,解构声明在这里可以正常工作。

注意: 你也可以使用标准类 Pair,让 function() 返回 Pair<Int, Status>,但通常把数据恰当命名会更好。

示例:解构声明与映射

遍历映射最优雅的方式大概是这样:

1
2
3
for ((key, value) in map) {
   // 对键和值做一些处理
}

要让它生效,你应该

  • 通过提供 iterator() 函数把映射表示为值的序列。
  • 通过提供 component1() 和 component2() 函数把每个元素表示为一个对。

而标准库确实提供了这样的扩展:

1
2
3
operator fun <K, V> Map<K, V>.iterator(): Iterator<Map.Entry<K, V>> = entrySet().iterator()
operator fun <K, V> Map.Entry<K, V>.component1() = getKey()
operator fun <K, V> Map.Entry<K, V>.component2() = getValue()

因此你可以在 for 循环中自由地对映射(以及数据类实例的集合或类似结构)使用解构声明。

用下划线表示未使用的变量

如果你不需要解构声明中的某个变量,可以用下划线代替它的名称:

1
val (_, status) = getResult()

对于以此方式跳过的组件,不会调用 componentN() 运算符函数。

lambda 中的解构

你可以在 lambda 参数中使用解构声明语法。如果 lambda 有一个 Pair 类型(或 Map.Entry,或任何具有相应 componentN 函数的其他类型)的参数,你可以把它们用圆括号括起来,用一个位置引入多个新参数:

1
2
map.mapValues { entry -> "${entry.value}!" }
map.mapValues { (key, value) -> "$value!" }

注意下面两种写法的区别:声明两个参数,与用一个解构的对替代一个参数:

1
2
3
4
{ a -> ... } // 一个参数
{ a, b -> ... } // 两个参数
{ (a, b) -> ... } // 一个解构的对
{ (a, b), c -> ... } // 一个解构的对和另一个参数

如果解构参数的某个组件未被使用,可以用下划线替换它,从而避免为它取名字:

1
map.mapValues { (_, value) -> "$value!" }

你可以为整个解构参数指定类型,也可以单独为某个组件指定类型:

1
2
3
map.mapValues { (_, value): Map.Entry<Int, String> -> "$value!" }

map.mapValues { (_, value: String) -> "$value!" }

基于名称的解构

实验性

Kotlin 支持基于名称的解构声明,其中变量按名称匹配属性,而不是按基于位置的解构中 componentN() 函数所定义的位置。

提示: 关于基于名称的解构的更多信息,请参阅该特性的 KEEP。

在基于位置的解构中,变量对应 componentN() 函数的顺序,例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
data class User(val username: String, val email: String)

fun main() {
    val user = User("alice", "alice@example.com")

    val (email, username) = user

    println(email)
    // alice

    println(username)
    // alice@example.com
}

在这个例子中,由于解构依赖 componentN() 函数的顺序,email 得到的是 username 的值,而 username 得到的是 email 的值。

在基于名称的解构中,决定提取哪些值的是属性名,而不是 componentN() 函数的位置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fun main() {
    val user = User("alice", "alice@example.com")

    // 使用显式形式的基于名称解构
    (val mail = email, val name = username) = user

    println(name)
    // alice

    println(mail)
    // alice@example.com
}

基于名称的解构是实验性特性。启用该特性后,它还会引入使用方括号的基于位置解构的新语法。对于元素顺序有意义的类型(例如列表和其他有序集合),以及像 Pair 或 Triple 这样的未命名元组,请使用该语法:

1
2
3
4
val point = Pair(10, 20)

// 使用基于位置解构
val [x, y] = point

你可以用 -Xname-based-destructuring 编译器选项控制编译器如何解释解构声明。

它有以下几种模式:

  • only-syntax 启用基于名称的解构的显式形式,但不改变现有解构声明的行为。
  • name-mismatch 在数据类中使用基于位置解构、而变量名与属性名不匹配时报告警告。
  • complete 启用带圆括号的简写形式基于名称解构,并继续支持用方括号语法的基于位置解构。

提示: 在启用 complete 模式之前,请先查看并解决 name-mismatch 模式报告的警告。这些警告会显示哪些解构声明在 complete 模式下会被编译器以不同方式解释,并给出相应改写建议。

如果你使用 complete 模式,带圆括号的简写解构语法会把变量与属性名匹配,而不再依赖位置:

1
val (email, username) = user

要在你的项目中启用基于名称的解构,请把编译器选项添加到构建配置文件中:

Gradle

1
2
3
4
5
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xname-based-destructuring=only-syntax")
    }
}

Maven

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<build>
    <plugins>
        <plugin>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-plugin</artifactId>
            <configuration>
                <args>
                    <arg>-Xname-based-destructuring=only-syntax</arg>
                </args>
            </configuration>
        </plugin>
    </plugins>
</build>