Kotlin 2.5 的 Companion 改造,对 KMP 的重要升级

看起来好像就是多了两种新写法 companion {} 和 companion fun Type.xxx():

  • 前者把 companion object 中的 object 去掉
  • 后者支持在类外给某个类型补充通过类名调用的方法

简单说的话就是语法糖更新,但是这个简单的"调整"在 KMP 工程上还有很有意义的,比如 JVM 产物可以直接用静态成员,然后类型级扩展不需要目标类预先声明 Companion,这样 KMP 也能有机会直接对齐其他平台的静态 API。

目前这个能力从 Kotlin 2.5.0-Beta1 开始通过实验特性提供。

首先是去掉了 object,也算是把类级方法编译成真正的静态成员,类似于当你需要在 companion object 里放工厂方法、常量和工具方法,比如 Vector.zero() 的是,以前这种代码背后会存在一个真正的 Companion 对象,JVM 侧通常会生成 Vector$Companion 类和对应的单例实例。

也就是就算你用了 @JvmStatic,额外生成的静态入口还是可能转调 Companion 实例上的方法,然后现在终于可以写:

kotlin 复制代码
class Vector(val x: Double, val y: Double) {
    companion {
        fun zero() = Vector(0.0, 0.0)
    }
}
​
val v = Vector.zero()

虽然调用侧几乎没什么变化,但是对编译侧来说变化还是很大的:

  • KEEP 规定 Vector.zero() 对应的方法直接生成在 Vector.class 上,成为 JVM static 方法,也就是不再需要为了这个 block 创建 Companion 对象
  • 编译器的 bytecode listing 已经能够看到 public final static method foo() 直接出现在类 C 里面
  • 对应的属性访问器也直接是 static

可以看到,旧写法保留 Companion 类型和对象,新写法可以直接落到外层类的静态方法,不过 companion {} 里的属性目前还有 backing field,初始化还要遵循 JVM 的 <clinit> 规则,所以减少 Companion 实例看起来也不是相关静态状态都消失了:

但是 companion block 没有对象身份,需要把 MyClass.Companion 作为值传递、让它实现接口或者参与继承的时候,还是应该保留 companion object。

另外现在不给源码加 companion object,也能给第三方类型补充类级 API ,比如旧的 fun Vector.Companion.unit() 必须先有一个 Vector.Companion 类型,如果类是 Java SDK 提供的,或者 Kotlin 类作者根本没声明 Companion,扩展就无从下手。

而新语法现在可以直接在文件顶层声明 companion fun Vector.unit(),不管目标类型来自 Kotlin 还是 Java,都不要求修改目标类。

用一个 Android BLE SDK 的包装方法举例,比如第三方 Java 库已经有 DeviceSession.open(String) 工厂方法,但它不认识业务二维码里的 ble:// 前缀,业务层又希望用 DeviceSession.fromQr() 创建会话,大概情况会是下面这里:

arduino 复制代码
// 第三方 Java SDK,无法修改
public final class DeviceSession {
    private final String id;
​
    private DeviceSession(String id) {
        this.id = id;
    }
​
    public static DeviceSession open(String id) {
        return new DeviceSession(id);
    }
​
    public String id() {
        return id;
    }
}
kotlin 复制代码
// Extensions.kt:应用自己提供的扩展
companion fun DeviceSession.fromQr(text: String): DeviceSession {
    require(text.startsWith("ble://"))
    val id = text.removePrefix("ble://")
    require(id.isNotBlank())
    return DeviceSession.open(id)
}
​
// 业务代码(需要使扩展进入作用域)
val session = DeviceSession.fromQr("ble://sensor-01")
println(session.id())  // sensor-01

可以看到,这个例子从二维码文本进入扩展方法,经过输入校验,调用原 Java 静态工厂,最后拿到一个真实的 DeviceSession 对象,而原有 Java 类完全不需要改动,调用方也不用为了一个工厂方法制造"假 Companion"。

实际上这对 JVM 来说更有意思:fromQr 实际会被编译到 ExtensionsKt.class 的文件级静态方法里面,扩展声明里的 DeviceSession 不会变成一个隐含的实例参数,更没有把新方法注入第三方的 DeviceSession.class:

不过这次扩展还改变了同名方法的解析规则 ,按照 KEEP 的设计,直接写 Example.test() 的时候,companion block 和 companion extension 一般优先于旧的 Companion 对象成员,所以如果确实需要旧方法,就要显式写 Example.Companion.test(),这对于包含大量扩展函数的可能还挺麻烦的。

编译器也明确限制了扩展接收者:它必须是可确定的类或者接口,不能随意写泛型类型参数、对象类型或者带具体类型实参的接收者,这些限制已有 FIR checker 实现:

最后,原有 KMP expect class 如果描述了 companion object 成员,就很难直接对应到已有 Java 类上的静态方法,因为双方的产物模型不同,而现在新的 companion block 被定义为与平台静态成员对齐:

  • KEEP 明确允许 Java 类的静态成员参和对应 Kotlin expect 的实际化
  • 在 JS 端映射为类的 static 成员
  • Swift 一侧规划把 companion block 暴露成静态类型方法或者属性,把 companion extension 暴露为 Swift extension

这对于已经有 Java SDK、又想设计 KMP 共用声明的库,这可以减少不少额外桥接层。

不过这个设计貌似看起来有一些工程取舍,比如相同的 Type.foo() 写法不代表相同的二进制位置:

  • 把 companion object 迁移成 companion block,成员由 Companion 实例方法变成外层类 static 方法
  • 把 block 内方法再移到文件级 companion extension,JVM 宿主类又会变成 FileKt

也就是源码调用可能没有变化,但已经发布库的 Java 调用方、反射逻辑、ABI 检查和旧二进制依赖都可能受到影响,如果目的只是让已有 Companion 方法方便 Java 调用,那还是继续使用 @JvmStatic 往往更稳妥。

比如在 Kotlin 2.5.0-Beta1 厘米,启用代码的 Gradle 配置可以这样写:

csharp 复制代码
kotlin {
    compilerOptions {
        freeCompilerArgs.add(
            "-Xcompanion-blocks-and-extensions"
        )
    }
}

如果只尝试 companion {},可以使用 -Xcompanion-blocks。

感觉这个如果 stable 之后, KMP 又可以更进一步

相关推荐
前端双越老师1 小时前
程序员 1v1 访谈系列第二期:很焦虑但又不知道干啥
前端·ai编程·全栈
广州华水科技2 小时前
北斗形变监测传感器在大坝安全监测中的应用与优势分析
前端
孔令飞2 小时前
如何学习Go
前端
HUIBUR科技2 小时前
AI 中枢,面向企业场景构建的 AI 智能体
前端·数据库·人工智能
随便写个软件2 小时前
【新版本】EasyExplorer资源管理器 3.12.0——地址栏重要更新
运维·服务器·前端·everything·资源管理器
Jack N2 小时前
【Strapi 系列之二】Collection / Single / Component 怎么选?Strapi 字段大全一篇讲透
前端·javascript·strapi
Kapaseker2 小时前
写了这么久 Kotlin,你真的理解 in、out 和 * 吗?
android·kotlin
hiahiahia1232 小时前
Agent Tool 执行失败怎么办
服务器·前端
Java后端的Ai之路2 小时前
09-React组件生命周期
前端·react.js·前端框架