Kotlin 2.3.20 现已发布!又有什么好东西?
大家吼哇,这次轮到 Kotlin 2.3.20 登场啦! 本次更新内容可以在 JetBrains 官方的 What's new in Kotlin 2.3.20 查阅, 我照例挑自己比较感兴趣的一些改动聊聊。
一句话总结:语言层面的新玩具不算夸张,但工具链、多平台和互操作体验又被打磨了一圈。对写库、折腾构建、做 KMP 的小伙伴来说,这次还挺香。
注意!这次依旧是「我个人 pick」的更新摘要,覆盖不了全部改动;如果你对某个方向特别感兴趣,记得继续深入官方文档喔。
文中示例如无特殊说明均来自或改写自官方日志。
语言特性
这次语言层面的 headline 不算特别多,不过上来这个我还挺喜欢的:按名称解构。
按名称解构(Name-based destructuring)
以前的解构是纯粹按位置来的,也就是说,只要顺序写错,变量名写得再漂亮也没用:
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
}
看起来像是拿到了 email 和 username,实际上完全是反的。
这种情况平时不一定天天能撞上,但只要字段一多、变量名一改,翻车概率就会直线上升。
现在,Kotlin 2.3.20 带来了实验性的按名称解构。显式写法像这样:
fun main() {
val user = User("alice", "alice@example.com")
(val mail = email, val name = username) = user
println(name)
// alice
println(mail)
// alice@example.com
}
简单来说,就是不再依赖 componentN() 的顺序,而是直接按属性名匹配。这个思路我觉得非常合理,
尤其是 data class 字段一长,或者你只是想起个本地变量别名的时候,确实顺手很多。
这个特性目前还是实验性的,可以通过编译器参数启用:
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xname-based-destructuring=only-syntax")
}
}
这个参数有几个模式:
only-syntax:只启用显式的按名称解构语法,不改变现有圆括号解构的行为。name-mismatch:如果位置解构时变量名和属性名对不上,会给 warning。complete:连传统的val (a, b)也会按名称来匹配,而按位置解构则改用方括号写法。
是的,complete 模式下会变成这样:
val [username, email] = user
有一说一,我第一眼看到方括号的时候脑子里闪过的不是“优雅”,而是“咦?这玩意居然真走到这一步了?”。 不过如果官方真打算长期往“默认按名称解构”这个方向推进,那这套语法倒也说得通。
上下文参数的重载解析调整
除了新特性,这次还有一个挺值得留意的行为变更:带 context parameters 的声明,不再天然比没有 context parameters 的声明更“具体”了。
以前有些重载组合在带上下文的时候会优先挑中带 context 的版本;
而从 2.3.20 开始,这种“优先照顾上下文版本”的规则没了。结果就是:
如果两个重载只有 context parameters 不同,那原本能编译的地方现在可能直接变成歧义错误。
官方示例大概是这种感觉:
class Logger {
fun info(msg: String) = println("INFO: $msg")
}
fun saveUser(id: Int) {
println("Saving user $id (no logger)")
}
context(logger: Logger)
fun saveUser(id: Int) {
logger.info("Saving user $id")
}
然后你在 context(logger) { saveUser(1) } 里调用时,2.3.20 会认为这俩候选谁都不该无脑赢,进而报歧义。
这类更新对普通业务代码的体感可能不一定很强,但对库作者、DSL 作者或者喜欢玩上下文参数的小伙伴来说,算是一个需要留意的兼容性点。
顺便一提,kotlin.context 相关重载数量也从 22 个缩到 6 个了,代码补全和解析压力理论上会更轻一些。
标准库
Map.Entry.copy():终于能放心把 Entry 拿出来用了
标准库这次给 Map.Entry 加了一个实验性的 copy() 扩展函数。别看名字普通,这玩意还挺实用。
以前如果你从 map.entries 里拿出一些 Entry,然后再去修改原 map,这些 Entry 往往就不太可靠了。
现在可以先 copy() 成一个不可变副本,再继续干活:
@OptIn(ExperimentalStdlibApi::class)
fun main() {
val map = mutableMapOf(1 to 1, 2 to 2, 3 to 3, 4 to 4)
val toRemove = map.entries
.filter { it.key % 2 == 0 }
.map { it.copy() }
map.entries.removeAll(toRemove)
println("map = $map")
// map = {1=1, 3=3}
}
这类 API 很像那种“你之前不一定天天想得起,但真写集合处理逻辑时会突然觉得:诶这个早该有了吧?”的补丁。
它也是实验性的,使用时需要 @OptIn(ExperimentalStdlibApi::class),
或者加编译器参数 -opt-in=kotlin.ExperimentalStdlibApi。
编译器插件
这次编译器插件相关有两个点,我觉得都值得一提。
kotlin.plugin.jpa 终于更像“开箱即用”了
之前如果你用了 kotlin("plugin.jpa"),它主要会帮你开 no-arg 那一套。
但大家都知道,JPA 这边除了无参构造,另一个老生常谈的问题就是:实体类得是 open 的,
不然懒加载之类的行为很容易歪掉。
现在 2.3.20 把这块也补上了:kotlin.plugin.jpa 会自动连带把 all-open 和新的 JPA preset 一起配置好。
也就是说,常见的这些注解:
javax.persistence.Entityjavax.persistence.Embeddablejavax.persistence.MappedSuperclassjakarta.persistence.Entityjakarta.persistence.Embeddablejakarta.persistence.MappedSuperclass
都会自动获得 open 和 no-arg constructor 的支持,不需要你再手搓一堆额外配置。
虽然我平时并不怎么写 JPA,不过这更新确实很实在。 这种“本来就应该自动帮你做好”的事情,越少让用户记配置越好。
Lombok 插件进入 Alpha
Lombok 编译器插件这次从 Experimental 升到了 Alpha。 对于纯 Kotlin 项目来说,这消息未必多激动; 但如果你是在 Kotlin/Java 混编、历史包袱又比较重的项目里打滚,那这玩意还是挺有存在感的。
官方把它推进到 Alpha,至少说明一个信号:他们是想认真把这条兼容路线继续往前走的,而不是一直把它摆在“试试看”状态。
Kotlin/JVM
这次 Kotlin/JVM 依旧在狠狠干一件很朴素的事:继续抹平和 Java 生态之间的各种细节摩擦。