Kotlin 1.3 前瞻之 Inline Class

Author Avatar
Dexlind 7月 03, 2018
  • 在其它设备中阅读本文章

啊啦啦,垃圾 Kotlin,你又偷拿人家东西了!

这几年各路语言的发展就是把 Haskell 吃剩下的再拿出来吃一遍,那么 Kotlin 也不例外(进入高级引战模式)。如果你精通 Haskell 或者 Scala,那太棒了,你不需要浪费时间阅读这篇辣鸡水文,请点击右上角的关闭按钮。

虽然说本文讲的是 Kotlin 1.3 版本的新特性,但是理论上 1.2.50 也能用(处于开发阶段,有大量 Bug,不建议日常使用)。如果想要体验这个实验性特性需要添加编译器参数 -XXLanguage:+InlineClasses。本文使用的 Kotlin 版本为 1.2.60-eap-7。

本文写于 Kotlin 1.2 的时代,由于 Inline Class 这个特性尚未正式实装,实际情况可能会有所变动,届时本文可能会有部分过时内容,然后针对 Kotlin 1.3 补充一些内容。

本文不会详细介绍各个细节,只抓重点讲。并且由于我太鶸,也许本文通篇错漏百出也说不定。

前言

JB他们在17年4月份的时候,做了一份调查问卷,Kotlin Future Features Survey,内容是选出最希望实装的语言特性(20选3),后来得出的调查结果是 Inline Class 排第五。现在这个功能得以实装也算是众望所归了。

不过吃惊的是我没想到JB那帮人做得那么快,我还以为这特性要等到 Kotlin 2.0 才能摸出来。

什么是 Inline Class

Inline Class,翻译成中文就是「内联类」。我们知道 Kotlin 有内联函数,可以消除函数调用的开销。那么内联类,则是可以消除创建对象的开销。

很强,是吧。如果不懂的话请往下看。

inline class Duck(val name: String)

以上代码就声明了一个最简单的内联类。

内联类必须有一个主构造函数,并且在主构造函数里必须有且只有一个 val 属性,除此之外,不能再拥有其他的字段。(var 属性目前还没做好所以不能用。)

可以给内联类添加泛型、次级构造函数、方法以及没有 backing field 的属性,还可以实现接口:

inline class Duck(val name: String) {
   constructor() : this("ywwuyi")

   fun talk(): Nothing = throw UnsupportedOperationException()

   var i: Int
      get() = 1551
      set(value) = println(value)

   companion object { /* ... */ }
}

使用起来就根普通的类一样:

fun test() {
   val duck = Duck("ywwuyi")
   println(duck.name)
   println(duck.i)
   duck.i = 6655
   duck.talk()
}

聪明的同学可能已经猜到了内联类的所谓「消除创建对象的开销」指的是什么。在经过编译器的处理后,以上代码就变成了下面这样:

fun test() {
   val duck = "ywwuyi"
   println(duck) // 输出 1551
   println(Duck$Erased.getI(duck))
   Duck$Erased.setI(duck, 6655)
   Duck$Erased.talk(duck);
}

整个类实例被“内联”掉了,只剩下它里面包裹的值。对成员方法的调用变成了对静态函数的调用(类似于扩展函数)。

也就是说,Inline class 可以给某个类型的值创建一个装箱类(就像上面的 Duck 包装了一个 Int 类型的值),并且这个装箱类能够被内联,“箱子”不复存在,只剩下那个被装箱的值。换句话说,内联类看起来就像是一个“零开销”的 wrapper。

用途举例

垃圾没用.jpg、进入胡说八道模式.jpg。

严格的类型别名

我们知道 Kotlin 有类型别名,能给某个类型取一个另外的名字。例如我们想用类型别名弄一个长度单位「米」:

typealias Meter = Double

但是 Meter 和 Double 其实是同一个类型,你并不能阻止将一个 Meter 赋值给一个 Double 类型的变量,你也并不能阻止两者相加。

val a: Meter = getMeter()
val b: Double = getMeter()
val c = a + b

但是如果这里使用内联类就不一样了:

inline class Meter(val value: Double) {
   operator fun plus(m: Meter) = Meter(this.value + m.value)
}

fun test2() {
   var a = Meter(1.0)
   val b = Meter(2.0)
   val c = a + b // ok, c的类型为Meter
   a = 3.0       // 编译错误:type mismatch
}

由于 Meter 是内联类,所以上面的代码实际上并不会创建 Meter 类的实例,在运行时只是在操纵 double,但是却保证了井水不犯河水,Meter 和 Double 各不相干。你甚至可以用内联类定义一套计量单位。

任何你想得到的包装类(wrapper)

“无痛”创建任何 wrapper,不用担心开销。

例如嫌原来的方法名字太丑,那就包一层:

interface Aa {
   fun a1()
   fun a2()
}

inline class Fuck(val a: Aa) {
   inline fun fuck1() = a.a1()
   inline fun fuck2() = a.a2()
}

例如属性代理:

inline class InlinedDelegate<out T>(val value: T): ReadOnlyProperty<Any?, T> {
   override inline operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
      // do everything you like
      return value
   }
}

class Test {
    val some: String by InlinedDelegate("just for test")
}

etc.

无符号整型

无符号整型(unsigned integer)也是 Kotlin Future Features Survey 里面出现过的 feature(第12号),由于内联类的实装,无符号整型因此可以很好地实现,而不会有过大的开销。

inline class ULong(private val data: Long) : Comparable<ULong> { /* balabala */ }

fun test() {
   val a = 2_147_483_648.toUInt() // a 的类型为 UInt
   val b = 1_000_000_000
   val c = a + b // c 的类型为 UInt
   println(c) // 3147483648
}

同时还新增了无符号整形字面量来简化声明。

// 结尾的u意味着这是一个无符号整形
val uintMask = 0xFFFF_FFFFu // UInt
val ulongUpperPartMask = 0xFFFF_FFFF_0000_0000uL // ULong

无符号整型将在 Kotlin 1.3 实装。

Try/Result Monad(伪)

函数式风格的错误处理,世面上已经有太多类似的玩意了(Arrow 的 Try,隔壁 Scala 的 Try[T],河对面 Haskell 的 Exceptional e t)。

所以 Kotlin 官方团队打算把类似的东西塞进标准库里,预计将在 Kotlin 1.3 版本实装。

inline class Result<T>(val value: Any?) {
   // 省略一万字
}

不过具体的情况还很微妙,Roman 大爷说并不打算做得和 Arrow 的那个一样,而是做成一个功能十分有限的玩意,连 flatMap 都要你自己写出来。没有 flatMap 的 Monad 还能叫做 Monad 么?

目前来看,只是用于精简协程中的 Continuation 接口用。

更多的用处由于篇幅关系省略不写,摸了~

补充:Kotlin 1.3 实装后,看起来野心蛮大,在之后的版本也许会针对 Result 重载 ?.!! 等操作符。

编译器的魔术

这里是 Kotlin 1.2 版本的内联类的实现细节。

在你声明一个内联类的时候,编译器会为你生成两个类,一个类是内联类本体,另一个是为内联进行支持的辅助类。例如:

inline class Dog(val name: String) : Cloneable {
   constructor() : this("quin")

   fun talk() = println("rua~")

   val next: Int get() = 22
}

经过编译器之手,就变成了如下这样(细节已省略):

inline class Dog(val name: String) : Cloneable {
   constructor() : this("quin")

   fun talk() = Dog$Erased.talk(name)

   val next: Int get() = Dog$Erased.getNext(name)

   fun unbox() = name
}
public static final class Dog$Erased {
   public static final void talk(String $this) {
      System.out.println("rua~");
   }
   public static final int getNext(String $this) {
      return 22;
   }
   public static final Dog box(String v) {
      return new Dog(v);
   }
}

对于用到内联类的地方,例如

fun test1(dog: Dog) {
   dog.next
   test2(dog, dog, dog)
}

fun <T> test2(dog1: Dog?, dog2: Cloneable, dog3: T) {
   dog1!!.talk()
   (dog3 as Dog).name
}

内联后变成这样(细节已省略):

fun test1(dog: String) {
   Dog$Erased.getNext(dog)
   test2(dog, Dog$Erased.box(dog), Dog$Erased.box(dog))
}

fun <T> test2(dog1: String?, dog2: Cloneable, dog3: T) {
   Dog$Erased.getNext(dog1!!)
   (dog3 as Dog).unbox()
}

从上面可以看出,并不是所有的用处都能内联掉,在需要的时候仍然会将其装箱。

更多的细节由于篇幅关系省略不写,摸了~

补充:对于 Kotlin 1.3,不会再生成一个名为 ***$Erased 的合成类,而是将这些静态方法放在了同一个类里。

补充说明

未来可能支持内联枚举(inline enum),就像这样(随便找了一段 Java 代码用 Kotlin 重写):

inline enum NavigationMode(val value: Int) {
   NAVIGATION_MODE_STANDARD(0),
   NAVIGATION_MODE_LIST(1),
   NAVIGATION_MODE_TABS(2);
}

abstract val navigationMode: NavigationMode

另外未来可能支持 reified 泛型参数,比如这样:

inline class Reified<reified T>(val some: T)

fun foo(a: Reified<Int>, b: Reified<String>)
// 编译后
fun foo(a: Int, b: String)

更多的补充说明由于摸了的关系省略不写~

知识共享许可协议
本作品采用知识共享 署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可,转载请注明出处。

本文链接:https://aisia.moe/2018/07/03/inline-class/