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。

由于 Inline Class 这个特性尚未正式实装,实际情况可能会有所变动,届时本文可能会有部分过时内容。

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

前言

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 属性,除此之外,不能再拥有其他的字段。

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

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

   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: UInt = 2_147_483_648.toUInt()
   val b: UInt = 1_000_000_000.toUInt()
   val c: UInt = a + b
   println(c) // 3147483648
}

不过目前该功能尚未完成,如果JB的 Kotlin 团队做得快的话,无符号整型就能在 Kotlin 1.3 里用上了。

Try/Result Monad(伪)

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

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

// 这个纯粹是夏姬八写了
inline class SuccessOrFailure<T, E: Exception>(val value: T | E)
// 代码仅供参考,请以实物为准

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

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

编译器的魔术

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

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()
}

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

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

内联类的一些限制

  • 内联类的主构造函数里的属性必须是 val,而不能是 var。
  • 内联类必须在顶层声明,而不能声明为嵌套类。
  • 内联类类型的 vararg 被禁用,例如 fun test(vararg dogs: Dog) {}

以上这三点限制可能在将来能够解除。

其他的限制由于篇幅关系省略不写,摸了~

未来可能支持内联枚举(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

Kotlin 团队:摸了~

补充说明

我知道你们想要这个

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

但是实际可能和你们想象的不一样

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

已经在做了.jpg

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

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

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