Kotlin 2.3.0 现已发布!又有什么好东西?

大家吼哇,这次轮到 Kotlin 2.3.0 登场啦! 本次更新内容可以在 JetBrains 官方的 What's new in Kotlin 2.3.0 查阅, 我照例挑自己最感兴趣的改动聊聊。

一句话总结:Java 25 终于支持,特性体验逐渐舒适。实用功能层出不穷,小伙伴们赶快更新~

注意!这次依旧是「我个人 pick」的更新摘要,覆盖不了全部改动;对其他领域感兴趣、但是我没提到的伙伴可以继续深入官方文档喔。

文中示例如无特殊说明均来自或改写自官方日志。

语言特性

一如既往先看语言层面,首先映入眼帘的是对一部分实验特性的转正,然后是一批新晋实验特性,最后是对 Java 25 的支持。

一如既往的方阵阵营。

嵌套类型别名 & when 数据流穷举转正稳定

之前在 2.2.x 里加入的「嵌套 typealias 支持」(Support for nested type aliases) 和「基于数据流的 when 穷举检查」(Data-flow-based exhaustiveness checks for when expressions) 转正咯。 现在写多层 typealias 不会再有警告, when 也会结合 smart cast 和 sealed 的上下文做更聪明的穷举判断了。

默认启用 suspend 解析 & 函数表达式里 return

注意:这个更新是在 2.3.0 的某个 EAP 版本中描述的,但是在 2.3.0 正式版更新中没有描述,因此它可能被移除了。

Kotlin 2.3.0 默认启用了两项之前需要 -language-version 2.3 的特性:

  • lambda 给既有 suspend 又有非 suspend 重载时,不再需要手动强转,直接写 suspend { } 就行。
  • 函数表达式里允许 return,只需显式标注返回类型。之前写 fun foo() = return 42 会报错,现在没事啦。

默认启用 body 中的 return 表达式特性

Kotlin 2.3.0 默认启用了之前 2.2.20 中更新的一个需要 -language-version 2.3 的特性:

在 body 表达式的局部使用 return。比如说:

Kotlin 复制代码
fun getDisplayNameOrDefault(userId: String?): String = getDisplayName(userId ?: return "default")

未使用返回值检查器

新增了一个 -Xreturn-value-checker ,可以提示你「调用了有意义的返回值却没用」。 可以用来提前发现那种「写了一大串表达式结果却丢了」的 bug。

例如:

kotlin 复制代码
fun formatGreeting(name: String): String {
    if (name.isBlank()) return "Hello, anonymous user!"
    if (!name.contains(' ')) {
        // 检查器会警告这个结果被忽略了
        "Hello, " + name.replaceFirstChar(Char::titlecase) + "!"
    }
    val (first, last) = name.split(' ')
    return "Hello, $first! Or should I call you Dr. $last?"
}

上面这段里,if 分支中构造了一段字符串却没有返回或赋值,检查器就会给出「结果被忽略」的警告。

默认情况下,这个检查器只对被标记了 @MustUseReturnValues 的作用域生效。 想要以 check 模式启用的话,可以在 build.gradle.kts 中这样写:

kotlin 复制代码
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xreturn-value-checker=check")
    }
}

然后通过注解来声明「这里的返回值必须被使用」。可以标记整个文件:

kotlin 复制代码
// 标记整个文件:文件里的函数/类返回值若被忽略则会被检查器提示
@file:MustUseReturnValues

package my.project

fun someFunction(): String

也可以只标记某个类:

kotlin 复制代码
// 标记整个类:类中所有函数的返回值如果被忽略都会被检查器提示
@MustUseReturnValues
class Greeter {
    fun greet(name: String): String = "Hello, $name"
}

fun someFunction(): Int = ...

如果你希望对整个项目的所有返回值都进行检查,可以开启 full 模式:

kotlin 复制代码
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xreturn-value-checker=full")
    }
}

在这个模式下,相当于所有编译结果都隐式带上了 @MustUseReturnValues 标记。

有些函数的返回值被忽略是很正常的,比如 MutableList.add,这类就可以用 @IgnorableReturnValue 标记掉:

kotlin 复制代码
@IgnorableReturnValue
fun <T> MutableList<T>.addAndIgnoreResult(element: T): Boolean {
    return add(element)
}

如果只是某一处调用想压制警告,又不想在函数签名上动刀,可以把结果赋值给下划线变量:

kotlin 复制代码
// 这是一个「不允许忽略返回值」的函数
fun computeValue(): Int = 42

fun main() {

    // 这里会有警告:返回值被忽略
    computeValue()

    // 这里不会有警告:显式把返回值丢给一个特殊的 unnamed 变量
    val _ = computeValue()
}

对于我这种偶尔写 DSL 忘记 return 的人来说,简直就是妥妥的保命符一张呀。

显式后备字段

还记不记得之前的版本想要写一个有「后备字段」的属性要怎么写?

Kotlin 复制代码
private val _city = MutableStateFlow<String>("")
val city: StateFlow<String> get() = _city

fun updateCity(newCity: String) {
    _city.value = newCity
}

而现在,可以不用这么麻烦了!

Kotlin 复制代码
val city: StateFlow<String>
    field = MutableStateFlow("")

fun updateCity(newCity: String) {
    // Smart casting works automatically
    city.value = newCity
}

使用 field = ... 的方式可以直接指定一个真正的后备字段,方便实用! 这个特性是试验性的,要开启它,添加编译器参数 -Xexplicit-backing-fields

Kotlin 复制代码
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xexplicit-backing-fields")
    }
}

上下文敏感解析继续打磨

目前还在 Experimental,这次限制了「只把密封类和当前类型的外部父类」加入上下文,从而减少盲目扩散。 如果你在类型运算里引进了容易撞名的类,编译器会给出新 warning,提示这段解析已经因为上下文分支而不再确定。

Kotlin/JVM:面向 Java 25

编译器现在可以输出 Java 25 的字节码了。对想第一时间尝鲜新 JDK API 的同学只需把 target 设到 25 就好, Gradle/IDE 也都打通了。

好耶!支持输出 Java 25 咯~

Kotlin/Native

一些 Kotlin/Native 的更新喔~ 我对 K/N 并不是非常熟悉,如果这部分有你非常感兴趣的内容,不妨也去看看官方的详细内容, 以防有什么遗漏~

Swift Export 更自然

虽然不太懂移动端开发,不过 Swift export 这轮带来了一些看似(?)很不错的点:

  1. 原生 enum class 终于会被映射成 Swift 的 enum,不用再接受那些 class 模板。
  2. Kotlin 的 vararg 直接翻译成 Swift 的 ... 变参,用 Swift 写调用端的时候自然顺滑。

比如官方文档里给出了这样一组 Kotlin / Swift 映射:

kotlin 复制代码
// Kotlin 端
enum class Color(val rgb: Int) {
    RED(0xFF0000),
    GREEN(0x00FF00),
    BLUE(0x0000FF)
}

val color = Color.RED
Swift 复制代码
// Swift 端
public enum Color: Swift.CaseIterable, Swift.LosslessStringConvertible, Swift.RawRepresentable {
    case RED, GREEN, BLUE

    var rgb: Int { get }
}

vararg 也会被翻译成 Swift 里的变长参数:

kotlin 复制代码
// Kotlin 端
fun log(vararg messages: String)
Swift 复制代码
// Swift 端
public func log(messages: Swift.String...)

要注意的是泛型 vararg 还没支持,但至少常见日志函数、多参数工具函数都没什么影响。

C 和 Objective-C 库导入进入 Beta

虽说我对 Kotlin/Native 不是非常熟悉,但是我知道 K/N 将 iOS 的开发放在首位,也一直在跟 Swift/Objective-C 进行搏斗、 改进它们之间的互调用与兼容性体验。 而这次,对 Swift/Objective-C 和 C 的库导入功能进入了 Beta 阶段,也算是一个阶段性突破了~

不过当然,这部分功能仍然处于实验性 阶段,仍然存在一些限制、以及需要标记 @ExperimentalForeignApi。 但终归是一次进步,不是吗?

Objective-C 头文件中块类型的默认显式参数名

Kotlin 函数类型中的显式参数名现在是 Objective-C 头文件导出的默认设置,改进了 Xcode 中的自动完成体验。 嗯... 也是对 Objective-C 的互调用与兼容性体验的一个内容。

Native 发布任务构建速度提升

这个则是对 K/N 整体的开发体验的提升。 官方提到:

根据基准测试,发布构建可以快高达 40%,具体取决于项目大小。这些改进在针对 iOS 的 Kotlin Multiplatform 项目中最为明显。

Apple 目标支持的变更

  • iOS/tvOS 最低版本从 12.0 提升到 14.0
  • watchOS 最低版本从 5.0 提升到 7.0
  • macosX64iosX64tvosX64watchosX64降级到支持层级 3
  • 计划在 Kotlin 2.4.0 中移除 x86_64 Apple 目标支持

时代在变迁、社会在进步。不过看到这些 X64 的平台被移到 Tier 3 还是不禁感叹: TMD 我什么时候才能有钱把我这个英特尔芯片的 Mac 给换了!

Kotlin/Wasm

Kotlin 2.3.0 默认为 Kotlin/Wasm 目标启用完全限定名,为 wasmWasi 目标启用新的异常处理提案, 并引入 Latin-1 字符的紧凑存储。

名字/异常更靠谱

  • KClass.qualifiedName 在 Wasm 目标上默认可用了,之前得手动开 flag ,而现在免配置了,也不会增大二进制。
  • wasmWasi 目标改用新版异常处理提案,和市面上主流 VM 的实现保持一致;wasmJs 还停留在 legacy 版本, 有需要可以自己加 -Xwasm-use-new-exception-proposal

Latin-1 字符的紧凑存储

以前,Kotlin/Wasm 按原样存储字符串字面量数据,这意味着每个字符都以 UTF-16 编码。 这对于仅包含或主要包含 Latin-1 字符的文本不是最优解。

从 Kotlin 2.3.0 开始,Kotlin/Wasm 编译器可以以 UTF-8 格式存储仅包含 Latin-1 字符的字符串字面量了。

这种优化显著减少了元数据,官方数据表示这个优化:

  • Wasm 二进制文件最多缩小 13%(与未优化版本相比)
  • 即使启用完全限定名,仍可缩小 8%

此功能默认启用,更新版本即可享受~


有一说一,K/Wasm 还有很多可以打磨的地方。继续加油!

Kotlin/JS:更少样板的互操作

更少样板的互操作优化!

直接导出 suspend

@JsExport 终于不再排斥 suspend 了,只需额外添加一个编译器参数:

kotlin 复制代码
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xenable-suspend-function-exporting")
    }
}

之后 Kotlin 的 suspend 会在 JS/TS 侧自动表现成 async/Promise,子类覆盖也照样写 async

我去,史诗级更新!但是似乎反而让我的编译器插件 kotlin-suspend-transform-compiler-plugin 的作用变小了... 欸?

启用之后,被 @JsExport 标记的 Kotlin suspend 函数就可以直接被 JS/TS 端当作 async 函数来用,例如:

kotlin 复制代码
@JsExport
open class Foo {
    suspend fun foo() = "Foo"
}
typescript 复制代码
class Bar extends Foo {
    override async foo(): Promise<string> {
        return "Bar"
    }
}

LongArray 映射到 BigInt64Array

给 JS Runtime 的 LongArray 现在会变成原生的 BigInt64Array,和需要 typed array 的 Web API 完全对接, 也能更轻松地把 Kotlin 模块暴露给外部。

使用编译器参数 -Xes-long-as-bigint 启用它:

kotlin 复制代码
kotlin {
    js {
        // ...
        compilerOptions {
            freeCompilerArgs.add("-Xes-long-as-bigint")
        }
    }
}

在那之前,Kotlin 会将其映射为 Array<bigint>

跨 JS 模块系统的统一伴生对象访问

以前,当使用 @JsExport 将带有伴生对象的 Kotlin 接口导出到 JavaScript/TypeScript 时, 在 TypeScript 中使用该接口的方式会因模块系统(ES 模块或其他)而异。

例如:

Kotlin 复制代码
@JsExport
interface Foo {
    companion object {
        fun bar() = "OK"
    }
}

调用的时候:

kotlin 复制代码
// 适用于 CommonJS、AMD、UMD 和无模块
Foo.bar()

// 适用于 ES 模块
Foo.getInstance().bar()

而现在,Kotlin 统一了所有 JavaScript 模块系统的伴生对象导出。 在 2.3.0 之后,对于每个模块系统(ES 模块、CommonJS、AMD、UMD、无模块),接口内的伴生对象总是以相同的方式访问(就像类中的伴生对象一样):

kotlin 复制代码
// 适用于所有模块系统
Foo.Companion.bar()

这个改进还顺便修复了集合类型互操作性。 比如集合工厂函数必须根据模块系统以不同方式访问:

kotlin 复制代码
// 适用于 CommonJS、AMD、UMD 和无模块
KtList.fromJsArray([1, 2, 3])

// 适用于 ES 模块
KtList.getInstance().fromJsArray([1, 2, 3])

现在也改过来啦:

Kotlin 复制代码
KtList.fromJsArray([1, 2, 3])

此功能默认启用,更新版本即可享受~

支持带有伴生对象的接口中的 @JsStatic 注解

之前的版本中 @JsStatic 注解不允许在导出的带有伴生对象的接口内使用。

例如,以下代码会产生错误,因为只有类伴生对象的成员才能用 @JsStatic 注解:

kotlin 复制代码
@JsExport
interface Foo {
    companion object {
        @JsStatic // 错误
        fun bar() = "OK"
    }
}

这种情况下你就不得不删除 @JsStatic 并用下述方式从 JS 访问伴生对象:

kotlin 复制代码
Foo.Companion.bar()

现在,带有伴生对象的接口支持 @JsStatic 注解了。 你现在可以在此类伴生对象上使用此注解,并直接从 JS 调用函数,就像对 class 那样:

kotlin 复制代码
Foo.bar()

此功能默认启用,更新版本即可享受~

@JsQualifier 注解可用于单个函数和类

以前,@JsQualifier 注解只能在文件级别应用,并要求所有外部 JS 声明放在单独的文件中。

从 Kotlin 2.3.0 开始,可以将 @JsQualifier 注解直接应用于单个函数和类了, 就像 @JsModule@JsNonModule 注解一样!

例如,现在可以在同一文件中将下述外部函数代码写在常规 Kotlin 声明旁边:

kotlin 复制代码
@JsQualifier("jsPackage")
private external fun jsFun()

此功能默认启用,更新版本即可享受~

支持 JavaScript 默认导出

之前的版本中 Kotlin/JS 无法从 Kotlin 代码生成 JS 的默认导出。 相反,Kotlin/JS 只生成命名导出,例如:

javascript 复制代码
export { SomeDeclaration };

如果需要默认导出,则必须使用变通方法,例如将 @JsName 注解与 default 加空格作为参数:

kotlin 复制代码
@JsExport
@JsName("default ")
class SomeDeclaration

有一说一不看这更新文档我都不知道还有这种变通方法...

而现在,可以通过新注解 @JsExport.Default 直接支持默认导出了! 应用于 Kotlin 声明(类、对象、函数或属性)时,生成的 JS 会自动为 ES 模块包含 export default 语句:

效果如下:

javascript 复制代码
export default HelloWorker;

此功能默认启用,更新版本即可直接使用注解 @JsExport.Default

标准库

标准库也迎来了一波转正与改进~

改进的 UUID 生成和解析

Kotlin 2.3.0 为 UUID API 引入了多项改进,包括:

  • 解析无效 UUID 时返回 null 的支持
  • 生成 v4 和 v7 UUID 的新函数
  • 为特定时间戳生成 v7 UUID 的支持

解析无效 UUID 时返回 null 的支持

2.3.0 增加了一些支持返回 null 的 API 。不看后面的文档我也能猜到, 肯定是添加了一些结尾是 orNull 的 API 🤓

  • Uuid.parseOrNull() -- 解析十六进制带短横线或纯十六进制格式的 UUID 时。
  • Uuid.parseHexDashOrNull() -- 仅解析十六进制带短横线格式的 UUID 时。
  • Uuid.parseHexOrNull() -- 仅解析纯十六进制格式的 UUID 时。

生成 v4 和 v7 UUID 的新函数

2.3.0 引入了两个用于生成 UUID 的新函数:Uuid.generateV4()Uuid.generateV7()

Uuid.random() 函数保持不变,仍然生成版本 4 UUID,就像 Uuid.generateV4() 一样。

为特定时间戳生成 v7 UUID 的支持

书接上文。对于 v7 UUID,2.3.0 还引入了新的 Uuid.generateV7NonMonotonicAt(...) 函数, 可以使用它为特定时间点生成 v7 UUID。

Uuid.generateV7() 不同,Uuid.generateV7NonMonotonicAt(...) 不保证单调排序,因此为同一时间戳创建的多个 UUID 可能不是顺序的。


这几个功能(或者说 UUID API)还是实验性的,使用它的时候需要 optIn 注解, 或添加编译器参数 -opt-in=kotlin.uuid.ExperimentalUuidApi

csharp 复制代码
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-opt-in=kotlin.uuid.ExperimentalUuidApi")
    }
}

Clock/Instant 转正

终于!现在 kotlin.time.ClockInstant 正式 Stable,可以放心在公共 API 里暴露和使用了。

Gradle: 新增生成源码 API

KotlinSourceSet.generatedKotlin 这个新 API 可以优雅注册「生成的源码」,IDE 也能区分、自动触发生成任务。 简单示例如下:

kotlin 复制代码
val gen = tasks.register("generator") {
    val output = layout.projectDirectory.dir("src/main/kotlinGen")
    outputs.dir(output)
    doLast {
        output.file("generated.kt").asFile.writeText(
            // language=kotlin
            """
            fun printHello() {
                println("hello")
            }
            """.trimIndent()
        )
    }
}

kotlin.sourceSets.getByName("main").generatedKotlin.srcDir(gen)

看起来是一个主要为了配合 KSP 的新功能。不过有一说一, KSP 的源码生成的检测(尤其是在 KMP 项目中)的相关体验确实有些一言难尽。希望这次可以有所改善吧。

Compose 编译器: Release 版也能看懂 Stacktrace

Compose 编译器插件现在会在 R8 混淆阶段顺便产出 group key 的 mapping, 搭配 ComposeStackTraceMode.GroupKeys 就算是 release 版的崩溃也能定位到哪个 @Composable 块。

要启用 group key stacktrace,可以在初始化任何 @Composable 内容之前加上一句:

kotlin 复制代码
Composer.setDiagnosticStackTraceMode(ComposeStackTraceMode.GroupKeys)

如果这套 mapping 机制在你项目里反而带来了一些构建上的问题,也可以直接在 composeCompiler {} 里完全关闭:

kotlin 复制代码
composeCompiler {
    includeComposeMappingFile.set(false)
}

有一说一,Compose 我只是勉强会用的程度,更别说调试了。

破坏性变更&弃用&文档更新

官方还列举了一些破坏变更和弃用的内容条目。 不过大多数内容是弃用的,并且仍然保持语言本身的向后兼容。因此如果你对这方面比较敏感或者有需求, 可以自行前往官方文档阅读并学习如何迁移。我比较懒,就不再重新列举一遍咯~

官方还列举了一些有关文档内容的更新,比如 KMP 的独立页面也整合进来了。 不过经常翻阅官方文档的小伙伴们肯定已经发现了,有兴趣的话可以直接去官方文档溜一溜~

尾声

到这里就基本整理完啦~ K/JS 能导出 suspend 函数以及对标准库的时间API的稳定对我个人来讲无疑是最喜欢的、也是最有帮助的。 你呢?你认为这次更新中有没有你心目中的「史诗级」?

相关推荐
a努力。3 小时前
小红书Java面试被问:ThreadLocal 内存泄漏问题及解决方案
java·jvm·后端·算法·面试·架构
测试人社区-小明3 小时前
涂鸦板测试指南:从基础功能到用户体验的完整框架
人工智能·opencv·线性代数·微服务·矩阵·架构·ux
serendipity_hky3 小时前
【go语言 | 第4篇】goroutine模型和调度策略
后端·性能优化·golang
前端不太难3 小时前
RN 遇到复杂手势(缩放、拖拽、旋转)时怎么设计架构
javascript·vue.js·架构
人机与认知实验室3 小时前
新型人机环境系统智能结构:动态隔离与协同优化的三元架构
架构
狂炫冰美式3 小时前
《预言市场进化论:从罗马斗兽场,到 Polymarket 的 K 线图》
前端·后端
云空3 小时前
《M-Robots OS:中国首个开源鸿蒙机器人操作系统》
机器人·开源·harmonyos
hahjee3 小时前
Go编写的ANSI终端颜色和样式控制库在OpenHarmony PC上的完整适配实战
开发语言·后端·鸿蒙
2501_924064113 小时前
2025年优测平台:微服务全链路性能瓶颈分析与最佳实践
微服务·云原生·架构·性能瓶颈·全链路性能