Kotlin 2.3.20 现已发布,来看看!

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

首先总结:语言层面的新玩具不算夸张,但工具链、多平台和互操作体验又被打磨了一圈。 对库作者、折腾构建、和做 KMP 的小伙伴来说,还是蛮不错的。

注意!这次依旧是「我个人」的更新摘要,覆盖不了全部改动;如果你对某个方向特别感兴趣,记得继续深入官方文档喔。

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

语言特性

这次语言层面的更新不算特别多,不过上来这个我还挺喜欢的:按名称解构

按名称解构(Name-based destructuring)

以前的解构是纯粹按位置来的,也就是说,只要顺序写错,变量名写得再漂亮也没用:

kotlin 复制代码
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
}

看起来是拿到了 emailusername,但实际到手上完全是反的。

现在,Kotlin 2.3.20 带来了实验性的按名称解构。显式写法像这样:

kotlin 复制代码
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 字段一长,或者你只是想起个本地变量别名的时候,确实顺手很多。 并且也不一定需要 operator 函数了,任何一个具有属性的普通函数都可以胜任。

这个特性目前还是实验性的,可以通过编译器参数启用:

kotlin 复制代码
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xname-based-destructuring=only-syntax")
    }
}

这个参数有几个模式:

  • only-syntax:只启用显式的按名称解构语法,不改变现有圆括号解构的行为。
  • name-mismatch:如果位置解构时变量名和属性名对不上,会给 warning。
  • complete:连传统的 val (a, b) 也会按名称来匹配,而按位置解构则改用方括号写法。

是的,complete 模式下会变成这样:

kotlin 复制代码
val [username, email] = user

有一说一,我第一眼看到方括号的时候脑子里闪过的不是"优雅",而是"咦?这玩意居然真走到这一步了?"。 不过如果官方真打算长期往"默认按名称解构"这个方向推进,那这套语法倒也说得通。 但这种东西也还是要观望一阵子。如果不管是构建数组/列表,operator fun get(...),索引取数之类的也全都用方括号了, 代码的可读性如何还有待商榷。当然,我对此持乐观态度:我乐于见到更多简化操作的语法糖出现。

上下文参数的重载解析调整

除了新特性,这次还有一个挺值得留意的行为变更:带 context parameters 的声明,不再天然比没有 context parameters 的声明更"具体"了。

以前有些重载组合在带上下文的时候会优先挑中带 context 的版本; 而从 2.3.20 开始,这种"优先照顾上下文版本"的规则没了。结果就是: 如果两个重载只有 context parameters 不同,那原本能编译的地方现在可能直接变成歧义错误

官方示例大概是这种感觉:

kotlin 复制代码
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() 成一个不可变副本,再继续干活:

kotlin 复制代码
@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}
}

它也是实验性的 ,使用时需要 @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.Entity
  • javax.persistence.Embeddable
  • javax.persistence.MappedSuperclass
  • jakarta.persistence.Entity
  • jakarta.persistence.Embeddable
  • jakarta.persistence.MappedSuperclass

都会自动获得 open 和 no-arg constructor 的支持,不需要你再手搓一堆额外配置。

之前用 Kotlin 搭配 JPA 的时候确实又遇到一些小小的问题,不支持这次更新会不会让使用 JPA 的过程更加丝滑。

Lombok 插件进入 Alpha

Lombok 编译器插件这次从 Experimental 升到了 Alpha。 对于纯 Kotlin 项目来说,这消息未必多激动; 但如果你是在 Kotlin/Java 混编、历史包袱又比较重的项目里打滚,那这玩意还是挺有存在感的。

官方把它推进到 Alpha,至少说明一个信号:他们是想认真把这条兼容路线继续往前走的,而不是一直把它摆在"试试看"状态。

但是我的观点还是:即然用了 Kotlin,那么就要尽量避免或趁早摆脱 Kotlin & Java 混编的局面,宁可把 Kotlin & Java 拆到不同的模块呢?

Kotlin/JVM

这次 Kotlin/JVM 依旧在狠狠干一件很朴素的事:继续抹平和 Java 生态之间的各种细节摩擦。

支持 Vert.x 的 @Nullable 注解

Kotlin 2.3.20 现在可以识别 io.vertx.codegen.annotations.Nullable 了。 如果你在和 Vert.x 相关的 Java API 打交道,这意味着 Kotlin 的空安全检查能更靠谱一些,默认会对空性不匹配给出 warning。

如果你想上更严格的模式,可以加:

kotlin 复制代码
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xnullability-annotations=@io.vertx.codegen.annotations:strict")
    }
}

Null-Safe 相关注解的支持越来越丰富啦。

支持 Java 的只读集合注解

另一个我觉得更直观的点,是支持 @Unmodifiable@UnmodifiableView 这两个 Java 注解了。

从 2.3.20 开始,如果 Java 声明返回的集合带了这些注解,Kotlin 会把它们当成只读集合来对待。 你如果硬要接到 MutableList 之类的类型上,就会收到类型不匹配 warning。 而这个 warning 计划在 Kotlin 2.5.0 升级成 error。

例如:

java 复制代码
// Java
public class Java {
    public static @UnmodifiableView List<Object> unmodifiableView() {
        return List.of();
    }

    public static @Unmodifiable List<Object> unmodifiable() {
        return List.of();
    }
}
kotlin 复制代码
fun main() {
    // Warning: Java type mismatch
    val mutableView: MutableList<Any> = Java.unmodifiableView()
    val mutableCopy: MutableList<Any> = Java.unmodifiable()
}

Kotlin/Native

Kotlin/Native 这次依然是熟悉的配方:工程向更新偏多,真正做 KMP/Apple 目标的人会更有感觉

当然,这就不是我熟悉的领域了,如果有哪里说错了还望海涵~

C / Objective-C 互操作的新模式

如果你的 KMP 库或应用里用了 cinteroppod(),这次可以关注一下新的 C / Objective-C 互操作模式。

官方给了一个实验性的 -Xccall-mode direct,目标是改善旧互操作机制带来的一些兼容问题,尤其是那种: 你用新 Kotlin 版本编译了一个带 C / Objective-C 互操作的 KMP 库,结果老版本 Kotlin 工程不好接的问题。

开启方式大概是这样:

kotlin 复制代码
kotlin {
    targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget>().configureEach {
        compilations.configureEach {
            cinterops.configureEach {
                extraOpts += listOf("-Xccall-mode", "direct")
            }
        }
    }
}

交叉编译检查器 & 禁用 Native Cache 的新 DSL

另外还有两个更偏工程味儿的更新:

  • 新增 crossCompilationSupported 检查器,用来判断当前 target 是否支持交叉编译。
  • 禁用 Native 编译缓存现在有了新的 DSL,而且必须写明版本和原因。

后者大概像这样:

kotlin 复制代码
disableNativeCache(
    version = DisableCacheInKotlinVersion.2_3_0,
    reason = "Cache bug",
    issue = URI("https://youtrack.com/YY-1111")
)

有一说一,这种"你可以关,但必须写清楚为什么关"的设计我觉得可以理解, 我听说 K/N 的 cache 关掉之后性能影响还蛮大的,最好别让它变成某种被到处复制粘贴的祖传配置。

Kotlin/Wasm

Kotlin/Wasm 这次属于继续闷声打磨性能和互操作体验。

字符串性能和编译性能继续提升

官方这次给了一组相当好看的数字:

  • 某些基准下字符串插值最高快 4.6 倍
  • KotlinConf 应用的 Wasm 产物体积大约缩小 5%
  • 一些 StringBuilder.append() / 字符串拼接场景至少快 20%
  • Clean build 时间提升 65%
  • 增量 build 提升 21%

原因大体上是两部分:

  • 运行时上,K/Wasm 开始更多利用 JS String builtins 来处理 kotlin.String
  • 编译器上,又做了一轮内存和编译流程优化

总之,静待 Wasm 的更多更新!

支持 @nativeInvoke

这次还给 wasmJs 目标加了一个实验性的 @nativeInvoke,可以把某些 external JS 对象当函数那样直接调用。

kotlin 复制代码
import kotlin.js.nativeInvoke

@OptIn(ExperimentalWasmJsInterop::class)
external class JsAction {
    @nativeInvoke
    operator fun invoke(data: String)
}

fun main() {
    val action = JsAction()
    action("Run task")
}

Kotlin/WasmJs 的互操作向来不如 Kotlin/JS 的,所以之类功能多一个爽一个呀。

Kotlin/JS

来到我比较感兴趣、但也不是很擅长的部分了。K/JS 这次的两个更新,一个偏互操作,一个偏构建链路。

终于可以从 TypeScript 实现 Kotlin 接口了

这点我觉得很有意思。以前 Kotlin 接口导出到 TS 后,你可以"看见它",但不能真的在 TS 里把它实现出来。 现在这条限制放开了。

例如 Kotlin 这边:

kotlin 复制代码
@JsExport
interface DataProcessor {
    suspend fun process(): String
}

@JsExport
fun registerProcessor(processor: DataProcessor) { /* ... */ }

TS 这边就可以这么写:

typescript 复制代码
import { DataProcessor, registerProcessor } from "my-kmp-library"

class JsonProcessor implements DataProcessor {
    readonly [DataProcessor.Symbol] = true

    async process(): Promise<string> {
        return "processed JSON data"
    }
}

registerProcessor(new JsonProcessor())

而且默认实现也不是完全没法复用。 虽然 TS 本身没有 Kotlin 这种接口默认实现的概念,但可以通过 DefaultImpls 去代理:

typescript 复制代码
class ConsoleLogger implements Logger {
    readonly [Logger.Symbol] = true

    log(): string {
        return Logger.DefaultImpls.log(this)
    }

    get prefix(): string {
        return Logger.DefaultImpls.prefix.get(this)
    }
}

总比没有强嘛!

开启方式也一如既往:

kotlin 复制代码
kotlin {
    js {
        generateTypeScriptDefinitions()
        compilerOptions {
            freeCompilerArgs.add("-Xenable-implementing-interfaces-from-typescript")
        }
    }
}

这个改动对那种"用 Kotlin 写核心逻辑,然后想自然暴露给 TS 生态"的场景帮助非常大,和上次更新的能直接导出 suspend 函数一样爽。

支持 SWC 作为转译平台

另一个点是 K/JS 现在开始实验性支持 SWC。

简单理解就是:Kotlin/JS 编译器以后可以更专注于产出现代 JS, 而把"把现代 JS 再转成老一点、更兼容的 JS"这件事交给专业工具 SWC 去干。

启用方式是在 gradle.properties 里加:

properties 复制代码
kotlin.js.delegated.transpilation=true

官方还提到,这会让后续支持更现代的 inline JS 语法、甚至基于 browserslist 的 DSL 变得更自然。

这年头谁还要去兼容IE啊!

Gradle & Maven

这次构建工具相关的内容不少,而且不少都还挺实用。

Gradle 9.3.0 兼容 & ABI 校验任务更顺手

  • Kotlin 2.3.20 兼容 Gradle 7.6.39.3.0
  • Binary compatibility validation 的任务名更新了,checkLegacyAbi 之类的名字改得更直白了
  • 如果你启用了 ABI 校验,跑 check 时现在也会自动带上 checkKotlinAbi

之前在 CI 里有时候就总是忘了还有个 checkLegacyAbi,还得事后补来着。

Kotlin/JVM 编译默认 Build Tools API

从 2.3.20 开始,Kotlin Gradle 插件里的 Kotlin/JVM 编译默认使用 Build Tools API 了。

Maven 项目配置

现在 Kotlin Maven 插件可以帮你自动配置源码目录和 kotlin-stdlib 依赖。

只需要在 pom.xml 里这样加一个 extensions

xml 复制代码
<build>
    <plugins>
         <plugin>
             <groupId>org.jetbrains.kotlin</groupId>
             <artifactId>kotlin-maven-plugin</artifactId>
             <version>${kotlin.version}</version>
             <extensions>true</extensions>
         </plugin>
    </plugins>
</build>

之后它会自动:

  • src/main/kotlinsrc/test/kotlin 当作源码目录
  • 在你没手动声明的情况下自动补上 kotlin-stdlib

如果你不想要这套"智能默认值",也可以在 properties 里关掉:

xml 复制代码
<project>
    <properties>
        <kotlin.smart.defaults.enabled>false</kotlin.smart.defaults.enabled>
    </properties>
</project>

说的好,但是我选择 Gradle。

其他值得一提的内容

官方这次其实还更新了不少更偏底层或更偏生态整合的东西,比如:

  • Build Tools API 的构建操作支持取消、支持更一致的指标采集
  • 构建工具可以更自然地配置编译器插件
  • Kotlin/Native 的一些目标支持策略继续调整
  • 文档又补了不少 KMP、Compose、Ktor、Exposed 的内容

另外,官方也列了一些破坏性变更和弃用,这里简单提两个我觉得比较容易让人留意的:

  • 实验性的 context receivers 正式不再支持了,现在请老老实实拥抱 context parameters
  • macosX64tvosX64watchosX64 这些 Intel Apple 目标继续往弃用方向推进;iosX64 暂时还留在 Tier 3

如果你正好在这些边边角角上踩着线,还是建议去官方文档把 breaking changes 那一节完整看一眼。

希望 macsX64 平台别废弃这么快,我还没钱买新机器😭😭😭

尾声

这次 2.3.20 更新的内容就到这里啦~

我个人最感兴趣的几个点,一个是按名称解构,另一个则是 K/JS 终于能让 TypeScript 正经实现 Kotlin 接口。 你呢?这次更新里有没有哪个点正好戳中你?

相关推荐
用户2018792831672 小时前
TabLayout被ViewPager2遮盖部分导致Tab难选中
android
始持2 小时前
第十二讲 风格与主题统一
前端·flutter
小码哥_常2 小时前
Room 3.0大变身:安卓开发的新挑战与机遇
前端
Wect2 小时前
LeetCode 53. 最大子数组和:两种高效解法(动态规划+分治)
前端·算法·typescript
始持2 小时前
第十三讲 异步操作与异步构建
前端·flutter
用户2058620985832 小时前
踩坑复盘:弃MySQL选PostgreSQL,地理数据存储终于不头疼了
后端
guchen662 小时前
异步编程优化:从底层源码看最佳实践
后端
小码哥_常2 小时前
Spring Boot异常处理:别被@RestControllerAdvice“坑”了!
后端
金銀銅鐵2 小时前
Byte Buddy 生成的类的结构如何?(第二篇)
java·后端