Ohhnews

分类导航

$ cd ..
Jetbrains Blog原文

Kotlin 引入基于名称的解构赋值:迈向更安全、灵活的开发体验

#kotlin#编程语言#解构赋值#软件开发#语法更新

TL;DR

  • 引入了新的“括号内 val”语法以支持基于名称的解构。此外,引入了使用方括号的新语法以支持基于位置的解构。
    • 两者目前均为实验性功能(通过编译器参数 -Xname-based-destructuring=only-syntax 启用),并将在未来的版本中变为稳定功能。
  • 在遥远的将来,用于解构的“括号外 val”语法的行为将从基于位置更改为基于名称。
    • 在默认行为发生变更之前将有一个漫长的迁移期,且现有的工具链已支持协助迁移。
    • 您现在就可以切换到新行为(使用 -Xname-based-destructuring=complete),但请注意其仍处于实验阶段。
  • 编译器附带了迁移辅助工具,将在未来几个版本中默认启用,距离新行为成为默认设置还有一段时间。
    • 您现在可以通过使用 -Xname-based-destructuring=name-mismatch 来启用这些辅助工具。

Kotlin 正在发生变化,名称将成为解构的核心。未来,val (name, age) = person 将从 person 值中提取 nameage 属性,而无需考虑它们定义的方式和顺序。这标志着与当前解构方式的转变,目前的解构主要依赖于位置。本篇博文解释了这一变更背后的逻辑、迁移策略以及 Kotlin 工具链如何对此提供支持。

为什么要按名称而非位置进行解构?

解构声明最常用于访问数据类的属性。例如,我们可以按如下方式定义一个 Person 类:

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

然后我们可以在一次操作中提取多个主要属性。这就是所谓的将值解构为其组件。

$ kotlin
fun isValidPerson(p: Person) {
  val (name, age) = p
  return name.isNotEmpty() && age >= 0
}

目前,解构是按位置进行的。我们在解构声明中引入的变量通常遵循数据类中属性的名称,但语言层面并没有这样的强制要求。

$ kotlin
// 这与上面的函数完全相同
fun isValidPerson(p: Person) {
  val (foo, bar) = p
  return foo.isNotEmpty() && bar >= 0
}

这种缺乏关联可能会导致问题,因为很容易无意中交换两个属性的顺序。这种错误可能因为类型不匹配而在稍后被捕获,但它出现的位置距离实际源头非常遥远。

组件与主要属性之间的关联方式也阻碍了重构。例如,如果我们无法将 age 属性修改为计算属性,同时又保持简洁的数据类语法。想象一下我们做了以下更改:

$ kotlin
data class Person(val name: String, val birthdate: Date) {
  val age = (Date.now() - birthdate).years
}

现在,每个解构声明中的 age 突然变成了 birthdate!显而易见,源码兼容性仍然可能实现,但您需要做更多的工作。

当前的解构方法也与抽象相抵触。如果我们把 Person 变成一个接口,之前的解构实例将不再有效。我们可以通过引入自定义的 component 函数来解决这个问题,但这通常被认为是高级用法。因此,大多数接口并不提供此类便利。

$ kotlin
interface Person {
  val name: String
  val age: Int

  operator fun component1(): String = name
  operator fun component2(): Int    = age
}

如果解构依赖于名称而非位置,这些问题就会迎刃而解。无论您是重新排列顺序、将计算属性改为主要属性(反之亦然),还是在类、接口或对象中定义属性,都不会产生影响。属性名称是一个稳定的特征,这意味着源码不需要进行任何更改。

新语法

您可以通过传递 -Xname-based-destructuring=only-syntax 作为 编译器参数 来启用新语法。

话不多说,让我们看看使用名称进行解构的新语法。与其在括号外使用单个 val,不如在括号内部为每个属性使用 val

$ kotlin
fun isValidPerson(p: Person): Boolean {
  (val name, val age) = p
  return name.isNotEmpty() && age >= 0
}

不出所料,在上面的示例中,我们编写 val nameval age 的顺序并不重要。这种新语法还支持重命名,以应对您想要定义的变量名称与您想要访问的属性名称不一致的情况。

$ kotlin
fun isValidPerson(p: Person): Boolean {
  (val age, val theName = name) = p
  return theName.isNotEmpty() && age >= 0
}

基于位置的解构在某些用例中仍然很重要。例如,PairTriple 在概念层面上并没有为其组件命名,我们也不打算要求在代码中充斥着 firstsecond。基于位置的解构也可用于集合,在这种情况下,没有可用的属性。基于位置的解构新语法使用方括号——这与即将推出的集合字面量语法相呼应。您可以选择将 val 放在方括号内还是外。

$ kotlin
fun isZero(point: Pair<Int, Int>): Boolean {
  val [x, y] = point      // 一种方式
  [val x, val y] = point  // 另一种方式
  return x == 0 && y == 0
}

所有这些新语法都可以在任何可以进行解构的地方使用,包括 Lambda 表达式和循环。

$ kotlin
// 建议用于遍历 Map 的新语法
for ([key, value] in map) {
  // 处理每个条目
}

person?.let { (val name, val years = age) -> "$name is $years years old" }

重申一遍:这全是语法。从 2.3.20 版本开始,编译器能够识别其含义,并且我们打算在功能达到稳定状态后保留此语法。

重新利用括号

在未来的某个时间点,我们打算让所有使用括号的解构都基于名称。您现在可以通过使用 -Xname-based-destructuring=complete 编译器参数来体验这一未来。

然而,如果您已经有一个项目,进行切换可能会产生重大影响。最明显的问题是解构停止工作,代码需要更新。更危险的是那些仍然有效但含义发生改变的解构声明。

因此,编译器在 -Xname-based-destructuring=name-mismatch 编译器参数下提供了一个迁移助手。启用后,如果基于位置和基于名称的解构行为不一致,或者代码在括号解构不再支持位置模式后将无法通过编译,编译器会给出警告。

$ kotlin
// 两者均接受且行为相同
val (name, age) = person

// 警告:两者均接受,但行为发生改变
val (age, name) = person

// 警告:仅基于位置的解构接受
val (personName, personAge) = person
// IDE 会建议潜在的修复方案
// - 重命名: (val personName = name, val personAge = age) = person
// - 方括号: val [personName, personAge] = person

未来展望

正如本文所暗示的,将有充足的时间来迁移到新的基于名称的解构。我们目前的时间表如下:

  • 从 2.3.20 版本开始,基于名称的解构处于实验阶段,意味着您需要专门的编译器参数才能使用它。
    • IntelliJ IDEA 的支持可能尚不完善,特别是在迁移方面。
  • 到 2.5.0 版本(2026 年底),该功能将变为稳定版。
    • 新语法将无需额外配置即可使用。
    • 编译器将开始报告迁移提示,IntelliJ IDEA 将包含检查和快速修复功能以协助此过程。
    • 该阶段大致对应编译器参数中的 name-mismatch,尽管我们可能会根据用户反馈对报告方式进行一些调整。
  • 到 2.7.0 版本(2027 年底),使用括号的解构将默认基于名称。
    • 您可以通过在编译器参数中使用 complete 来提前迁移到此阶段。

这是一个巨大的变化,我们不想操之过急。如果在 2027 年期间的任何时候发现生态系统尚未准备好,我们可能会将此变更推迟到另一个主要版本。

我们绝不会弃用为数据类生成 component 函数的功能。数据类仍将生成相同的字节码——基于名称的解构是针对使用侧的功能。然而,我们计划引入不带 component 函数的多字段值类。这意味着值类的解构将仅基于名称。

参考资料