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

📅 发布时间:2026/10/15 14:04:02
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 实例上的方法然后现在终于可以写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上成为 JVMstatic方法也就是不再需要为了这个 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()创建会话大概情况会是下面这里// 第三方 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; } }// 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 实现最后原有 KMPexpect class如果描述了companion object成员就很难直接对应到已有 Java 类上的静态方法因为双方的产物模型不同而现在新的 companion block 被定义为与平台静态成员对齐KEEP 明确允许 Java 类的静态成员参和对应 Kotlinexpect的实际化在 JS 端映射为类的 static 成员Swift 一侧规划把 companion block 暴露成静态类型方法或者属性把 companion extension 暴露为 Swiftextension这对于已经有 Java SDK、又想设计 KMP 共用声明的库这可以减少不少额外桥接层。不过这个设计貌似看起来有一些工程取舍比如相同的Type.foo()写法不代表相同的二进制位置把companion object迁移成companion block成员由 Companion 实例方法变成外层类 static 方法把 block 内方法再移到文件级 companion extensionJVM 宿主类又会变成FileKt也就是源码调用可能没有变化但已经发布库的 Java 调用方、反射逻辑、ABI 检查和旧二进制依赖都可能受到影响如果目的只是让已有 Companion 方法方便 Java 调用那还是继续使用JvmStatic往往更稳妥。比如在 Kotlin 2.5.0-Beta1 厘米启用代码的 Gradle 配置可以这样写kotlin { compilerOptions { freeCompilerArgs.add( -Xcompanion-blocks-and-extensions ) } }如果只尝试companion {}可以使用-Xcompanion-blocks。感觉这个如果 stable 之后 KMP 又可以更进一步