Kotlin 新特性抢先看:伴生扩展与伴生块
大家吼哇!不知道大家是否了解 Kotlin 即将推出的一个新的实 验性特性:「Companion extensions and blocks(伴生扩展与伴生块)」呢?
哦我的上帝!对于这个特性,我可是相当期待,就像我无时无刻不在期待乔治叔叔的姐姐那新鲜出炉的苹果派一样。
单看名字也能看得出来,这个特性似乎是 companion object 的亲戚。而失去了 object,它又能玩出什么花样?今天我打算挑出这个特性聊聊。
想写个扩展,怎么还得看类的脸色?
首先我们来假设这里有一个表示坐标的 Point,我作为第三方使用者,想给它加个工厂方法 Point.origin(),用来创建一个原点。
老办法大家应该不陌生,给伴生对象写扩展就行:
// 原类型
class Point(val x: Int, val y: Int) {
companion object
}
// 第三方补充
fun Point.Companion.origin(): Point = Point(0, 0)
// 调用处
val point = Point.origin()
看着似乎一切都那么美好:Point 负责表示坐标,扩展负责补一个创建原点的方法,互不耽误。
但很明显这里有个前提:原本的 Point 类里得先有那句 companion object。
如果它不存在呢?Point.Companion 都没了,扩展自然也就无从谈起。你可能就只能写一个顶层的 createPointOrigin 之类的函数来作为代餐了。
自己写的类还好办,添一句就是了。可如果是别人库里的类,总不能为了加个扩展,先去求人家留一个伴生对象吧?甚至如果是面对一个 Java 中定义的类,就更是无从下手了。
这正是原有的伴生对象扩展的限制。它也被列在 KEEP 0449 的第一个问题里,对应的 KT-11968,倒也算是个老生常谈的问题了。
伴生扩展能做到什么?
而现在,伴生扩展(companion extensions) 带来了新的写法:
class Point(val x: Int, val y: Int)
companion fun Point.origin(): Point = Point(0, 0)
val point = Point.origin()
在顶层扩展函数前面加上 companion,接收者写成 Point,就这么多。
类里不用预留 companion object,也不用为了这个方法改动原来的类。
终于可以随心所欲地直接写
Point.origin()了,芜湖!
一个值得注意的点:这里扩展的是通过类型名调用的那一部分。这也是为什么我上面提到接受者时用了斜体,因为它不同于以前我们所熟知的那个 receiver:
这里没有一个 Point 对象被传进 origin(),因此它所代表的是一个“类型标记”,而不是一个参数、一个对象实例,调用时也不能换成 point.origin(),函数体内也无法使用 this。
具体规则可以看 KEEP 0449 的伴生扩展章节和 KEEP 0449 §1.3 的声明规则。
伴生扩展也同样支持属性:
companion val Point.UnitX: Point
get() = Point(1, 0)
用的时候通过 Point.UnitX 拿到一个 x 方向的单位坐标,point.UnitX 则不行。
当然了,扩展还是扩展,私有成员的访问限制依旧生效,很好理解也很合理的限制。
Java 的类,也能安排上
我们前面提到了 Java 的类,而伴生扩展也完美解决了对 Java 类型添加扩展的痛点。
比如给 LocalDate 加一个方法,把 20261009 这样的字符串解析成日期:
package example.dates
import java.time.LocalDate
companion fun LocalDate.fromCompact(text: String): LocalDate {
require(text.length == 8)
return LocalDate.of(
text.substring(0, 4).toInt(),
text.substring(4, 6).toInt(),
text.substring(6, 8).toInt(),
)
}
换个包,导入后就能用了:
import example.dates.fromCompact
import java.time.LocalDate
val date = LocalDate.fromCompact("20261009")
LocalDate 根本没有 Kotlin 的 Companion,现在也可以进行扩展了。
KEEP 0449在开头就特意提到了 Java 类型,也算是这次改动的重头戏之一。
顺便留意那个 import example.dates.fromCompact。它跟普通顶层扩展的规则是类似的。
不过 KEEP 确实也有提到过探索自动导入的想法,不过并未落地,或者至少现在没有落地。
那 Java 代码是不是也能直接调用 LocalDate.fromCompact() 了?
那当然...是不行的。了解 Kotlin 语言特性以及这些顶层函数的底层编译结果的小伙伴应该很容易理解,这类扩展并没有改写目标类型的字节码, 因此在 Java 那边肯定是不能直接调用的。至于该怎么调用,等到下文看编译结果就明白了。
扩展属性,居然还能存东西?
普通扩展属性有个规矩:不能有 backing field,也就是不能有一个真实储存内容的字段。所以通常它们只能写 getter/setter,想直接 val/var xxx = ... 是不行的。
但这次的伴生扩展属性不同:它允许有初始化器,也允许有 backing field。
companion val Point.UnitY: Point = Point(0, 1)
跟前面示例代码中的 UnitX 对比一下就能看出来:
UnitX每次进 getter,都会新建一个Point。UnitY在初始化时创建,之后访问拿到的是同一个对象。
看上去是 Point 的属性,但没有给每个 Point 实例塞一个字段。
参照它的 初始化规则,在 JVM 中它类似于一个放在扩展所在文件里的顶层属性。
泛型也能用,但不能这么用
再来一个 List 的例子:
companion fun <T> List.singleton(value: T): List<T> = listOf(value)
val names: List<String> = List.singleton("Kotlin")
注意这里是 List.singleton,不是 List<T>.singleton。泛型 T 是这个函数自己的,而不能是类型的。
KEEP 0449 §1.3.2对接收者的要求中提到:得是已经声明的类、接口,或者符合条件的类型别名,不能带指定的类型实参,也不能拿类型参数或 object 来充数。
这其实也很好理解。将伴生扩展相关的函数想象成 Java 中的静态函数(当然它实际上也是这么做的),你在Java中是不是也不能用 List<T>.of() 这种语法?