语言特性演进、预览特性与迁移判断

Kotlin 语言持续演进。官方文档既有稳定语法页面,也有语言特性提案和版本发布说明。学习 Kotlin 时不能只看“某个语法能不能编译”,还要判断它处于什么稳定级别、是否需要编译器参数、是否适合放进公共 API。

本章不是完整 release notes,而是把当前文档库中容易分散的版本相关语言特性串起来,帮助你在项目里做取舍。

如何阅读特性状态

官方语言特性页面通常会把特性分成几类:

状态 含义 项目建议
Stable 语义稳定,适合常规使用 可以纳入团队编码规范
In preview / Experimental 可试用,但语义或开关可能变化 应用内部谨慎使用,公共库 API 避免依赖
Exploration / KEEP discussion 仍在设计讨论 只用于了解方向,不写生产代码
Revoked 已撤回或被替代 不再新增使用,迁移到替代方案

Java 对比:Java 也有 preview features,需要显式开启并承担升级风险。Kotlin 的差异是很多能力通过 -X... 编译器参数、@OptIn 或语言版本控制分阶段开放。

languageVersion 不是 Kotlin 插件版本

项目使用 Kotlin 2.4 编译器,并不等于所有模块都必须使用 Kotlin 2.4 语言语法。可以用 languageVersion 限制源码特性:

kotlin {
    compilerOptions {
        languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2)
        apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2)
    }
}

这常用于大型项目渐进升级:先升级编译器和插件,获得 bug 修复与性能改进,再逐步允许新语言特性进入代码库。

近年稳定特性速览

特性 官方状态 本文档位置 适用场景
..< open-ended range Stable 区间、跳转、解构与 this 表达式 半开区间,替代部分 until
data object Stable 对象声明、伴生对象与对象表达式 密封层级中的无数据单例状态
Definitely non-nullable types Stable 类型系统与空安全 / 泛型 Java 互操作与泛型空性边界
Enum.entries Stable 数据类、密封类与枚举 values() 更高效地枚举常量
Guard conditions in when Stable 控制流 带主语 when 中按类型匹配后追加条件
Non-local break and continue Stable 控制流 / 区间、跳转、解构与 this 表达式 inline lambda 中跳出外层循环
Multi-dollar string interpolation Stable 字符串与数组 JSON Schema、模板字符串中大量 $ 字符
嵌套 typealias Stable 序列、类型别名与内联值类 收敛内部复杂类型名,避免包级污染
显式 backing field Stable 属性 外部只读、内部可变的属性存储
注解 use-site target 改进 Stable 注解 Java 框架、Bean Validation、records 互操作
Context parameters Stable 上下文参数与类型安全构建器 显式建模上下文依赖、DSL 环境能力

注意:发布说明描述的是某个版本发布时的状态;语言特性页面描述的是当前状态。一个特性可能在 Kotlin 2.2 是 preview,但在后续版本稳定。

Guard conditions:减少分支内部嵌套

旧写法:

fun label(value: Any): String =
    when (value) {
        is String -> {
            if (value.isBlank()) "空字符串" else "字符串:$value"
        }
        else -> "其他"
    }

使用 guard condition 后:

fun label(value: Any): String =
    when (value) {
        is String if value.isBlank() -> "空字符串"
        is String -> "字符串:$value"
        else -> "其他"
    }

它的优势不是“少几行”,而是把匹配条件放在分支头部,让分支选择规则更清楚。适合类型匹配后再判断状态的场景,例如 UI state、sealed hierarchy、错误分类。

Non-local breakcontinue

Kotlin 的 inline 函数允许某些非局部控制流。稳定后的 non-local breakcontinue 让循环中调用 inline lambda 时也能表达跳转意图。

示意:

for (line in lines) {
    line.forEach { char ->
        if (char == '#') continue
        if (char == '!') break
        print(char)
    }
}

使用前要确认团队成员能读懂控制流。很多时候,把逻辑拆成普通 for 循环或提取函数会更直观。

Java 对比:Java lambda 不能直接 breakcontinue 外层循环。Kotlin 这里依赖 inline 语义,不是所有 lambda 都支持。

Multi-dollar string interpolation

普通字符串模板用 $ 触发插值:

val name = "Ada"
println("Hello, $name")

当字符串里有大量字面量 $,例如 JSON Schema、Shell、模板语言时,传统写法需要大量 ${'$'},可读性很差。multi-dollar string interpolation 允许你指定“几个连续 $ 才触发插值”:

val className = "User"

val schema = $$"""
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://example.com/user.schema.json",
  "title": "$$className"
}
"""

这里 $$""" 表示两个 $ 才触发插值,单个 $schema$id 会保留为字面量。

实践建议:

  • 普通业务字符串继续使用单 $ 模板。
  • 只有当字符串中大量出现 $ 字面量时,再使用 multi-dollar。
  • 在团队代码规范中说明这种写法,否则读者可能误以为是拼写错误。

Context-sensitive resolution

Context-sensitive resolution 让编译器在已知期望类型时推断枚举项或密封层级成员,减少重复限定名。

传统写法:

enum class Problem {
    CONNECTION, AUTHENTICATION, DATABASE, UNKNOWN
}

fun message(problem: Problem): String =
    when (problem) {
        Problem.CONNECTION -> "连接失败"
        Problem.AUTHENTICATION -> "认证失败"
        Problem.DATABASE -> "数据库异常"
        Problem.UNKNOWN -> "未知问题"
    }

启用该特性后,在期望类型明确的位置可以写:

fun message(problem: Problem): String =
    when (problem) {
        CONNECTION -> "连接失败"
        AUTHENTICATION -> "认证失败"
        DATABASE -> "数据库异常"
        UNKNOWN -> "未知问题"
}

截至官方当前特性状态页,context-sensitive resolution 仍属于 preview,需要通过编译器参数试用:

kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xcontext-sensitive-resolution")
    }
}

它依赖上下文类型信息,例如:

  • when 的主语类型。
  • 显式返回类型。
  • 显式变量类型。
  • is 检查和转换。
  • sealed hierarchy 的已知类型。
  • 参数声明类型。

官方页面也说明,它不适用于函数、带参数的属性或带接收者的扩展属性。

实践建议:如果它仍处于 preview,就不要在公共库文档和核心 API 示例中大量使用。完整限定名虽然啰嗦,但跨版本和跨团队阅读更稳定。

注解目标规则与 @all

Kotlin 属性可能对应多个 JVM 元素:构造参数、字段、getter、setter、Kotlin property 元数据、record component。较新的注解目标规则更贴近 Java 框架期望,也支持 @all: 把注解传播到多个相关位置:

data class RegisterRequest(
    @all:Email
    val email: String
)

@all: 不是万能替代:

  • 不能一次包多个注解写成 @all:[A B]
  • 不能用于委托属性。
  • 不会传播到类型、扩展接收者或 context parameters。

Java 框架互操作时,最稳妥的策略仍是理解框架读取哪里,然后显式写 @field:@get:@param:@all: 适合你确实希望多个目标都带同一注解的场景。

嵌套 typealias

嵌套 typealias 已稳定后,可以把内部算法别名放到类或对象内部:

class PathFinder {
    data class Node(val id: String)

    private typealias Queue = ArrayDeque<Node>
}

不要把它当成“内部类替代品”。typealias 不创建新类型,只是给已有类型起别名。需要类型安全时仍应使用 data class、普通类或 inline value class。

显式 backing field

显式 backing field 让“公开只读类型,内部可变存储”更紧凑:

class Registry {
    val names: List<String>
        field = mutableListOf()

    fun register(name: String) {
        names.add(name)
    }
}

如果你的项目还要支持旧 Kotlin 版本,传统 backing property 更稳:

class Registry {
    private val _names = mutableListOf<String>()
    val names: List<String> get() = _names
}

公共库要考虑调用方的 Kotlin 版本基线,不要只因为新语法更短就立刻写进稳定 API 示例。

预览特性的工程策略

团队引入预览或实验性语言特性前,建议回答这些问题:

  • 是否需要 -X... 编译器参数?
  • 是否会出现在公共 API、KDoc、教程示例或生成代码中?
  • IDE、CI、静态分析、格式化工具是否都支持?
  • 如果下一版语义变化,迁移范围有多大?
  • Java 调用方是否会看到不同字节码形态?
  • 是否可以先限定在一个内部模块试用?

建议做法:

  • 应用内部可以试点,但要在构建脚本旁边写清楚原因。
  • 公共库 API 默认只使用稳定特性。
  • 示例代码面向初学者时,避免过早使用预览语法。
  • 每次升级 Kotlin 插件时,复查 freeCompilerArgs 中所有 -X 参数。

参考

  • 官方语言特性与提案状态:https://kotlinlang.org/docs/kotlin-language-features-and-proposals.html
  • Kotlin 2.2.0 更新说明:https://kotlinlang.org/docs/whatsnew22.html
  • 官方控制流文档:https://kotlinlang.org/docs/control-flow.html
  • 官方字符串文档:https://kotlinlang.org/docs/strings.html
  • 官方类型别名文档:https://kotlinlang.org/docs/type-aliases.html
  • 官方属性文档:https://kotlinlang.org/docs/properties.html
  • 官方注解文档:https://kotlinlang.org/docs/annotations.html