Kotlin 2.4.20 现已发布,新特性多不多?

大家吼哇!Kotlin 2.4.20 已经在 9 月 7 日发布啦~ 老规矩,更新的原文在这里:What's new in Kotlin 2.4.20

这次先在开头快速省个流:

  • 标准库层面的新特性(或者说新语法)并没有那么多,有相当一部分笔墨在 debug 和 test 上;不过标准库还是补了几个挺顺手的函数的。
  • 在 K/JS 方面的 suspend 导出也又有了新进展。
  • 还有一个让人眼前一亮的东西:Kotlin 编译器自己的 Native Image。哦?

注意!这里依旧主要介绍我个人感兴趣的更新,不保证会覆盖所有内容。如果有你关心但没提到的,记得去官方日志看看喔~下文中的代码示例,如无特殊说明,均来自或改写自官方更新日志。

Kotlin/JVM

when 的 invokedynamic 编译支持稳定

先来看看过去版本的试验性功能的稳定化。 还记得 v2.2.20 的时候,when 可以借助 invokedynamic 来生成更紧凑的字节码了吗? 现在这个特性已经稳定并默认启用 了,不再需要 -Xwhen-expressions=indy

比如这种按类型分支的代码:

kotlin 复制代码
open class Shape
class Circle : Shape()
class Rectangle : Shape()
class Triangle : Shape()

fun countCorners(shape: Shape) = when (shape) {
    is Circle -> 0
    is Rectangle -> 4
    is Triangle -> 3
    else -> -1
}

编译目标为 JVM 21 或更高版本 时,符合条件的 when 会使用 invokedynamic 配合 SwitchBootstraps.typeSwitch(),代替原本的一串 instanceof 检查。

当然了,一切优化行为总会有其条件与限制。除了 else 之外,分支要求需要是 isnull 检查,且不能带 守卫条件 if, 也不能涉及 MutableList、函数类型这类无法直接进行类型检查的类型;此外还需要至少两个条件,并且都检查同一个主体。 具体限制可以看官方提供的文档说明

有优化就是好事儿!

标准库

元素都相同?有没有重复?

不知道你有没有写过这种代码或类似的逻辑:

kotlin 复制代码
val noDuplicates = values.distinct().size == values.size

只是想知道列表中有没有重复的元素,但是总是要转来转去的。 这次标准库给这类需求加了几个专门的函数:

函数 用来检查什么
allDistinct() 元素是否互不相同
allDistinctBy() 按 selector 取出的值是否互不相同
allEqual() 元素是否全部相等
allEqualBy() 按 selector 取出的值是否全部相等

集合、序列和数组都可以使用。举个例子:

kotlin 复制代码
@OptIn(ExperimentalStdlibApi::class)
fun main() {
    data class Response(
        val participantId: String,
        val answer: String,
        val responseDate: String,
    )

    val responses = listOf(
        Response("P001", "Yes", "2026-09-08"),
        Response("P002", "Maybe", "2026-09-08"),
        Response("P003", "No", "2026-09-08"),
    )

    // 有没有人重复提交?这里每个人都不同。
    println(responses.allDistinctBy { it.participantId })
    // true

    // 大家的答案一样吗?
    println(responses.allEqualBy { it.answer })
    // false

    // 是不是同一天提交的?
    println(responses.allEqualBy { it.responseDate })
    // true

    val answers = responses.map { it.answer }
    println(answers.allEqual())
    // false
    println(answers.allDistinct())
    // true
}

Kotlin 的标准库已经是我用着最爽的标准库之一了,尤其是这些极其丰富地扩展函数。

这几个函数目前还是实验性的 ,需要 @OptIn(ExperimentalStdlibApi::class), 或者使用编译器参数 -opt-in=kotlin.ExperimentalStdlibApi

断言失败后构建提示信息

kotlin.test 这次也补了一个很朴素(但也很细节)的功能:断言的错误信息可以通过 lambda 延迟生成了。

在之前:

kotlin 复制代码
assertEquals(expected, actual, "Unexpected value for items: ${items.joinToString()}")

不管断言成不成功,这个字符串都会先拼好。要是错误信息里面还有一大坨对象格式化或者字符串的组合十分复杂,那么测试通过的时候这些性能和消耗就白白浪费掉了。

现在可以这样:

kotlin 复制代码
@OptIn(ExperimentalKotlinTestApi::class)
fun testValues(actual: Int, expected: Int, items: List<String>) {
    assertTrue(actual > 0) { "Expected a positive value but got $actual" }

    assertEquals(expected, actual) {
        "Unexpected value for items: ${items.joinToString()}"
    }
}

只有断言失败,才会执行后面的 lambda。 如果你平时用 JUnit 5,应该对这种写法不陌生,现在 kotlin.test 也跟上啦。

新增重载包括 assertTrue / assertFalseassertEquals / assertNotEqualsassertSame / assertNotSameassertIs / assertIsNot,以及 assertNullassertContainsassertContentEquals。 它们目前也需要 @OptIn(ExperimentalKotlinTestApi::class)

老实说,我之前写测试的时候经常以为是存在 assertTrue(...) { "message" } 这种语法的,所以总是写完了然后 IDE 给我报了错我才发现没有这种尾随 Lambda。 现在好了,不怕写"错"了。

为协程堆栈(coroutine stack trace)恢复提供异常副本

接下来这个对库作者应该更有吸引力一些。

众所周知协程的一个比较常见的痛点:协程中的异常可能在一个协程里抛出,再由另一个协程重新抛出,进而导致堆栈的丢失,增加调试难度。 为了方便排查,kotlinx.coroutines 会通过堆栈恢复,把相关的协程调用信息补到异常里。这个过程需要创建一个新的异常实例。

如果你的异常只有 message、cause 之类的常规构造参数,协程库可以自动处理。 但如果还带着错误码、行号或者其他什么自定义的额外信息,它就没法随便帮你构造了。

现在标准库新增了 StackTraceRecoverable 接口,让异常类型自己说明该怎么复制:

kotlin 复制代码
import kotlin.coroutines.ExperimentalStdlibCoroutineSupportApi
import kotlin.coroutines.debug.StackTraceRecoverable

@OptIn(ExperimentalStdlibCoroutineSupportApi::class)
class FileEditException private constructor(
    val line: Int,
    private val detail: String,
    cause: Throwable?,
) : IllegalStateException("When editing line $line: $detail", cause),
    StackTraceRecoverable<FileEditException> {

    constructor(line: Int, detail: String) : this(line, detail, null)

    override fun copyForStackTraceRecovery(): FileEditException =
        FileEditException(line, detail, this)
}

可以看到,复制时可以把 linedetail 保留下来,同时把原异常作为新异常的 cause。 这个函数一般由协程库在恢复堆栈时调用,不需要在业务代码里手动调。如果不希望异常被复制,也可以在实现中返回 null

这其中的一个细节是:这个接口在标准库里 。 也就是说,一个定义异常的基础库,不用为了配合协程堆栈恢复,再额外依赖 kotlinx.coroutines 了。

不过有两点要注意:这个 API 目前是实验性的 ,需要 ExperimentalStdlibCoroutineSupportApi 的 opt-in; 另外,虽然接口在所有目标上都可用,但 kotlinx.coroutines 目前只在 JVM 上用它来恢复堆栈。

Kotlin/JS

接下来看看我比较感兴趣但又不那么熟悉和擅长的 K/JS。这几次更新一直在补导出能力,这次也不例外。

支持导出 suspend lambda

之前已经能把 suspend 函数导出给 JS / TS 使用了,但如果函数的参数里还有一个 suspend lambda 呢? 比如你想让调用方传一个异步任务进来,然后在 Kotlin 这边调用它。

这次支持了这个能力:

kotlin 复制代码
@JsExport
class TaskRunner {
    suspend fun runTask(task: suspend () -> String): String {
        return task()
    }
}

TypeScript 侧可以直接传一个 async 函数:

typescript 复制代码
import { TaskRunner } from "my-kmp-library"

const runner = new TaskRunner();
const result = await runner.runTask(async () => "done");
console.log(result); // "done"

这个就很顺手了!高阶函数本来就是 Kotlin 的常客,如果一碰上 suspend 参数就导出不了,公共 API 设计时难免束手束脚,更别说作为中间层的跨语言协作了。

它目前还是实验性的,需要启用编译器参数:

kotlin 复制代码
kotlin {
    js {
        compilations.all {
            compileTaskProvider.configure {
                compilerOptions {
                    freeCompilerArgs.add("-Xsuspend-lambda-exporting")
                }
            }
        }
    }
}

data class 的忽略导出终于管全了

假如一个导出的 data class 里带了一个不打算给 JS 使用的类型,你尝试用 @JsExport.Ignore 忽略了构造函数和对应属性:

kotlin 复制代码
class DatabaseConnection

@JsExport
data class Session @JsExport.Ignore constructor(
    val userId: String,
    @JsExport.Ignore val connection: DatabaseConnection,
)

这时候编译器还是会给你警告:An internal type that isn't exported to JavaScript, 这是因为 data class 生成的 copy()componentN() 还是存在,并且又把这些被忽略的东西带出来了。

2.4.20 修复了这个问题,现在判断这些自动生成函数的导出情况时,也会考虑 @JsExport.Ignore 了。

不知道 Kotlin 什么时候出一个比 data class 更加轻量级的'数据承载类'语法呢?

浏览器测试的新 DSL

K/JS 过去通过 Karma 来组织浏览器测试,但 Karma 已经弃用一段时间了。 这次 Kotlin Gradle 插件提供了一套新的实验性 DSL,背后由 Playwright 管理和驱动浏览器,Mocha 跑测试,webpack 负责打包。

可以在 browser {} 里面添加新的 test {}

kotlin 复制代码
import org.jetbrains.kotlin.gradle.ExperimentalJsTestDsl
import kotlin.time.Duration.Companion.seconds

kotlin {
    js {
        browser {
            @OptIn(ExperimentalJsTestDsl::class)
            test {
                timeout = 2.seconds
                headless = true

                chromium {
                    timeout = 5.seconds
                }
                firefox()
                webkit()
            }
        }
    }
}

从配置就能看出来,可以按浏览器分别设置超时,也支持 Chromium、Firefox、WebKit 几种引擎。 不过老实说,对 Web 的这些什么测试啊之类的配置,我也只停留在了会配置 上,具体有啥区别我是一点儿不懂( 不管怎样,详细的运行方式可以参阅 官方文档

顺便一提,官方说后续打算把这里的 webpack 换成 Vite。注意,是后续计划 ,这次还没换。 原话是:webpack as a bundler (will be replaced with Vite in future releases). 期待一手!

Kotlin/Wasm

新的编译模式

Wasm 这次加了几种编译模式。以前是把项目和依赖一起编译成一个完整的二进制文件,这样方便做全程序优化、去掉用不到的声明,产物也比较小。 这种模式叫 monolith,并且依然是现在的默认值。

但是不得不说,一整个二进制文件的尺寸也确实有点儿大。

现在,新增的两种模式则会拆出多个二进制文件:

模式 编译方式 产物和优化
monolith 项目和依赖一起编译 单个二进制文件,可以对整个程序做优化
multimodule-open-world 每个模块独立编译,只重编变化的模块 每个模块有独立的二进制文件,不做跨模块优化,体积会更大
multimodule-closed-world 一次调用处理所有模块,只重编变化的模块 多个相互依赖的二进制文件,会移除不可达声明,但分别优化各个二进制文件

如果想尝试,可以在 gradle.properties 中配置,例如:

properties 复制代码
kotlin.wasm.compilationMode=multimodule-open-world

不过我觉得更值得试试的是另一种配置:

properties 复制代码
kotlin.wasm.compilationMode=multimodule-closed-world-only-in-dev

它会在开发构建中用 closed-world 多模块编译,生产构建则用 monolith。

老实说,Wasm 项目我写的不多,至于具体哪个选择更好,可能需要根据你项目的实际情况和真实的评测,来进行一定程度的取舍吧。

wasmWasi 支持 Wasmtime

以前 Kotlin Gradle 插件给 wasmWasi 提供的运行时只有 Node.js,需要借助 JS 引导代码来运行。 现在加上了 Wasmtime,可以直接使用独立的 WebAssembly 运行时:

kotlin 复制代码
kotlin {
    wasmWasi {
        wasmtime()
    }
}

配上之前对 WebAssembly Component Model 的探索,Kotlin/Wasm 在浏览器之外的玩法也越来越多了。

未来可期!

Lambda 和函数式接口的产物变小

这次还改了 lambda 和函数式接口的编译方式:不再各自生成单独的匿名类,改为生成函数并使用共享的基类。

官方在 KotlinConf 应用上的测试结果是,Wasm 二进制体积减少了大约 5%~10%。 当然,这个数字只是针对特定应用的参考,并不代表每个项目的实际情况。 而且这个变化会引入更多动态调用,同样有可能影响到运行时的性能,体积和运行效果还得结合自己的项目的实际情况进行测试与评估。

另外,父类和子类的 companion object 初始化顺序也调整了,现在会先初始化父类的 companion object,与 JVM 上的行为保持一致。

Kotlin/Native

哈哈,终于轮到我最不熟悉的环节了

Swift / iOS 甚至是整个 K/N 都不是我熟悉的方向,就不展开讲的过于具体了。如果有哪里说错了也望海涵~

Swift export 支持 sealed 层次结构

Kotlin 的 sealed class / interface 很适合配合 when 做穷举判断, 但是导出到 Swift 之后,依然还得写一个 default,很是糟心。

这次 Swift export 会为 sealed 类型生成 sealedType() 方法,返回一个对应的 Swift enum, 其中的 case 对应 Kotlin sealed 层次结构的直接子类型。Swift 侧就可以对它做穷举的 switch 了。

要留意这里的调用是 switch shape.sealedType(),不能直接把 Kotlin 的 sealed 类型当成 Swift enum 来写。如果层次结构更深,也可以嵌套调用。

跨语言继承

现在可以在 Kotlin 侧声明接口和一个 open 基类,再让 Swift 类继承这个基类、实现接口,最后再把实例传回接收该接口的 Kotlin 函数。 Kotlin 调用接口方法时,就会调用到 Swift 的实现。

这个用法适合接入那些不能直接导入 Kotlin 的纯 Swift 库:公共代码只依赖 Kotlin 接口, 实际的调用放在 Swift 实现里。官方用 CryptoKit 给了一个 完整例子

这俩特性现在都还属于仍在 Alpha 阶段的 Swift export,感兴趣的话可以试试。

klib 增量编译进入 Beta

Kotlin/Native 的 klib 增量编译这次推进到了 Beta,修了一些问题,也做了性能改进。 它最早在 1.9.20 引入,主要用于减少 debug 构建的编译时间。

目前仍然需要在 gradle.properties 中开启:

properties 复制代码
kotlin.incremental.native=true

默认启用是后续版本的计划,这次还是需要自己加配置喔。

另外,如果导出的 XCFramework 依赖 SwiftPM 包,assembleSharedXCFramework 任务现在会帮你生成配套的 Package.swift, 方便和 XCFramework 一起分发。不过生成文件之后,相应 SwiftPM 包的发布还是要做的。

native 的性能改进,依旧稳步进行。

Gradle & Build Tools API

这次 Kotlin Gradle 插件的完全兼容范围是 Gradle 7.6.3~9.7.0

Problems API 的集成也有一点改进:编译器现在会把诊断 ID 一起提交并分组,更方便定位编译问题的来源。 这套集成从 Gradle 8.6 开始默认开启,不过 API 本身还在演进,官方建议使用较新的 Gradle 来获得完整的改进体验。

Build Tools API(也就是 BTA),这次新增了对 K/JS、K/Wasm 和 Kotlin metadata 的实验性支持。 之前主要在 JVM 上使用,现在更多目标也可以通过这套统一的接口与编译器交互了。

要体验的话,在 gradle.properties 里添加对应目标的配置:

properties 复制代码
kotlin.wasm.runViaBuildToolsApi=true
kotlin.js.runViaBuildToolsApi=true
kotlin.metadata.runViaBuildToolsApi=true

这三个目标目前都需要手动开启,官方计划从 v2.5.0 开始默认启用。

Kotlin 编译器

编译器的 Native Image

v2.4.20 提供了首个实验性的 Kotlin 编译器 Native Image ,可以作为标准 kotlinc 命令行工具的替代品。

注意:这里说的是编译器本身的 Native Image,不是说把你的程序编成 Native Image。

它主打更快地启动速度和更好的性能。对于经常通过命令行调用编译器的情况听上去还蛮诱人的,大概? 不过具体能快多少,他们似乎没有提供相关数据?至少更新文档里没提。

镜像还内置了一些编译器插件,包括 Serialization、Compose compiler、All-open、no-arg、 SAM with receiver、Assignment、Lombok 和 Power-assert,可以通过 -Xplugin-Xcompiler-plugin 使用。

想尝试的小伙伴可以去 GitHub Release 下载, 具体用法可以参考它的 README

kotlin runner 改名

命令行用户还需要留意一个名字变化:Kotlin runner 的命令由 kotlin 改为 kotlinr, 目的是避免和 Kotlin Toolchain 里的 kotlin 命令冲突。继续用原来的命令时,runner 会给出警告,建议改用 kotlinr

不过我并不是命令行用户,跳过跳过~

破坏性变更和弃用

Wasm 不再暴露的内部入口

以前在 @JsFun 里可以直接调用顶层 require(),是因为编译器在 import-object.mjs 里生成了一个 require 变量。 但这个其实是意外暴露出来的,现在移除了。现在的话下面的声明会报错:

kotlin 复制代码
@JsFun("(mod) => require(mod)")
external fun loadModule(mod: String): JsAny

你需要改为使用 @JsModule 来导入模块,更符合 K/JS 期望的标准用法。

另一个是生成的 JS wasmExports API 开始弃用:除了暂时保留、会给出警告的 wasmExports.memory, 其他导出都禁止访问。要获取模块的 WebAssembly.Memory 对象,应改用 kotlin.wasm.unsafe.wasmMemory

有一说一,没用过。

webpack 升级带来的变化

这次 webpack 的 npm 依赖更新到了 5.108.1,有些可能影响现有配置的东西:

  • 内置压缩器依赖从 terser-webpack-plugin 换成了 minimizer-webpack-plugin。Terser 仍然是默认的压缩器,但如果你做了什么配置之类的,可能需要检查一下是不是还正常。
  • webpack 判断模块类型时不再忽略 import.meta。这方面如果出现了问题可能需要注意下。

Native 的两个限制

  • watchosArm32 目标弃用,计划在 2.5.0 移除,以适配 Xcode 27。Apple 的 32 位 watchOS 支持也要退出历史舞台了。(千万不要太快把 macOSX64 也斩杀掉好吗😭 就先保持着只是废弃的状态不要移除好吗,我现在还买不起新电脑...)
  • Kotlin/Native 编译器禁止在 public inline 函数中使用 AtomicFU 原子操作;如果 internal inline 函数被其他文件调用,也同样受此限制。

其他兼容性细节可以继续查阅官方的兼容性指南

尾声

这次的更新就先看到这里啦~

标准库和语言语法层面的东西不太多,更多的依旧是一些更加底层的改进。 JS 方面的导出和工程方面的内容一直在优化,也许会在未来带来更舒适的全栈开发或多端协同!?

native 也是依旧对 iOS 着重优化,以及各种性能改进。但是:你什么时候把完整好用的 kotlinx-io 正式版给我端上来😢😢😭

你呢?这次有没有哪个更新正好解决了你之前碰到的问题?

相关推荐
摇滚侠1 小时前
《SpringBoot 3:入门与应用实战》第 12 章 JDBC 与事务 使用 JdbcTemplate 阅读笔记 31
android·spring boot·笔记
举个栗子。1 小时前
Marin:开源基础模型研究与开发框架,从数据到模型全链路可复现
人工智能·ai·开源
Zeaon2 小时前
别再手写筛选栏了:一个可组合的 Flutter 选择组件(5 入口 × 7 委托)
android·flutter
Michaelwubo2 小时前
mysql8 主从 HA切换
android·adb
openEuler社区2 小时前
openEuler 社区 2026 年 1-2月运作报告
开源·操作系统·openeuler·openeuler社区运作报告
墨狂之逸才3 小时前
Android 工业手持机蓝牙扫描失败:为什么同时需要蓝牙和定位权限
android
ZStack开发者社区3 小时前
虚拟化观察 第 001 期:ZSvirt 核心 IaaS 引擎开源,VMware Explore 2026 开幕,Proxmox VE 8 正式 EOL
架构·开源·云计算·vmware·云基础设施·proxmox
Android打工仔3 小时前
Kotlin 协程源码解析(七)`delay()` 到底是如何在 `Dispatchers.Main` 上恢复的?
android·kotlin
大牧师3 小时前
MySQL 学习教程
数据库·sql·mysql·docker·node·全栈·后端数据