目录

kotlin value关键字以及inline class初探

inline Modifier

介绍

inline class 最早在 kotlin 1.2.30 中出现, 在 1.4.30 中到达 beta 版, 在 1.5 中可能变为 stable, 本文代码基于 Kotlin-1.5.0-M1.

可能很多 Javaer 并不会去在意 kotlin 那么多繁杂的关键字, 但我更喜欢追求高效率, 高易用性, 简洁舒适的代码, 好了, 我们来看看我们为什么要使用 inline class 呢?

我们知道, 在 JVM 中, 内存的储存是下面这样的:

https://cdn.jsdelivr.net/gh/zsqw123/cdn@master/picCDN/20210323232512.png

当我们创建一个基本类型的局部变量的时候, int / float / boolean 等类型将会被储存在 jvm 的内存栈中, 这些基础类型储存在栈上, 访问/创建以及储存他们的开销并不会很大.

而当我们实例化一个对象的时候, 该对象实例就会储存在 JVM 堆上, 而堆内存的使用代价很高(详见下方 benchmark 部分), 虽然每个对象可能给我们的感觉不算很大, 但是大量的对象堆积起来对性能的影响不容轻视, 我们看下面一个例子:

我们定义了一个方法, 它需要传入一个时间参数:

1
fun fk(time: Int) {}

这时候就出现了一个问题, 这个 time 究竟是小时? 分钟? 秒? 我们仅仅知道这是一个 Int, 为了让编译器可以强制指定, 我们可以使用强类型语言的优势, 将函数这样封装, 我们就只能传入 Minute, 因为它不支持传入其他类型

1
2
3
4
class Minute(val v: Int)
fun fk(time: Minute) {
    println(time.v)
}

但是我们会发现在大多数开源库的源码中都不会选择这样的做法, 因为这样假如我们时间的类型是 Hour, 我们还需要自己去 Hour 的类里面定义一个 toMinute 的方法, 但这样就会创建了一个我们并不会用到的对象 Hour. 因此大多数开发者往往会定义一个 TimeUnit 作为额外的参数传入, 这样的做法我们已经习惯了, 但是我们能否做出改变呢?

在 kotlin 这里, 答案是肯定的: Inline Classes

1
2
3
inline class Hour(private val v: Int) {
    fun toMinutes() = Minute(v * 60)
}

那么这个关键字做了什么呢? 我们可以打开 kotlin 的字节码并将其反编译为 java, 我们就能发现其内部操作的实质:

1
2
int hour = Hour.constructor-impl(1);
int var1 = Hour.toMinutes-impl(hour).getV();

我们来对比一下不使用 inline 关键字的时候字节码, 就知道发生了什么事情:

1
2
Hour hour = new Hour(1);
int var1 = hour.toMinutes().getV();

我们发现, 不使用 inline 时, 额外创建了一个 Hour 对象, 这也就是性能开销的原因了, 我们示例这个对象还比较简单, 试想如果在 Android 开发中, 你创建了一个 View 对象, 可想而知性能开销有多么恐怖, 当然, 在少量的情况下你不会感觉到性能区别有多大.

那么我们来解释一些东西, 我们首先实例化一个类:

1
val hour = Hour(1)

事实上, 在使用了 inline 的情况下, 就 JVM 而言, 其内部会变成类似于这样的代码:

1
int hour = 1;

而我们上面实例中的 fk 函数:

1
fun fk(time: Minute) {}

也会变成类似于如下的代码

1
void fk(int time){}

那么这时候就会出现一个问题, Hour 中的 toMinute() 方法怎么办?? 我们继续反编译其字节码, 我们会发现:

1
2
3
4
@NotNull
public static final Minute toMinutes_impl/* $FF was: toMinutes-impl*/(int $this) {
   return new Minute($this * 60);
}

我们会发现! 它直接返回了一个 Minute 对象, 如果 Minute 我们也将其变成 inline, 那么字节码会变成下面这样:

1
2
3
public static final int toMinutes_SCGyLbI/* $FF was: toMinutes-SCGyLbI*/(int $this) {
   return Minute.constructor-impl($this * 60);
}

当然, 这样的 inline 到最后一层使用的时候还是会变成一个对象的(并不一定), 但是我们确实做到了减少了一个对象的创建!

不过其实如果我们最后需要使用的是原始类型的话, 我们倒也可以自己定义一个函数实现这种转换:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
val hour = Hour(1)
val second = hour.toMinutes().toSecond()

inline class Hour(val v: Int) {
    inline fun toMinutes() = Minute(v * 60) // 这里的 inline 加不加性能影响微乎其微
}

inline class Minute(val v: Int){
    inline fun toSecond() = v * 60 // 这里的 inline 加不加性能影响微乎其微
}

对于 JVM, 就类似于这样:

1
2
val hour = 1
val second = 1 * 60 * 60

事实上反编译字节码我们发现是这样的:

1
2
3
int hour = Hour.constructor-impl(1);
int $this$iv = Minute.constructor-impl(hour * 60);
int second = $this$iv * 60;

通过字节码, 我们还可以了解到这个 constructor-impl 实际上就是内部参数本身:

1
2
3
public static int constructor_impl/* $FF was: constructor-impl*/(int v) {
   return v;
}

因此我们发现, 我们在并没有创建任何对象的情况下调用了两个对象的方法, 这样好!

限制

那么既然这么好, 那大家都用 inline 吧! 但是事实上, inline 有很多的限制

1
2
3
4
5
6
inline class Seconds()             		 	// nope - 需要接收并只能接收一个参数!
inline class Minutes(value: Int)  		 	// nope - 参数必须要同时声明为成员!
inline class Hours(var value: Int) 		 	// nope - 成员必须为只读的!
inline class Days(val value: Int)   		// yes!
inline class Months(private val count: Int) // yes! - 可以为任意属性名 也可为私有成员!
inline class Years private constructor(val value: Int) // nope - 必须为共有构造函数

我们发现, 主构造器只能接受一个基础值作为成员属性, 但是它的内部是可以拥有成员属性的, 只要它们仅基于构造器中那个基础值计算, 或者从可以静态解析的某个值或对象计算(单例,顶级对象,常量等)

1
2
3
4
5
6
7
object Conversions {
    const val MINUTES_PER_HOUR = 60    
}

inline class Hours(val value: Int) {
    val valueAsMinutes get() = value * Conversions.MINUTES_PER_HOUR
}

还有更多的限制: 不允许继承, 但可以实现接口, 必须声明为顶层函数, 嵌套/内部类无法内连, 不支持枚举内联类, 具体可以看文章末尾的参考链接

Type Alias 不香吗

Type aliases 提供了我们另一种访问一个类的方式, 就像下面这样, 我们可以给 String用其他的名字, 但事实上这样编译后还是原来的类型, 只是换了一种名字罢了, 我们传入不是这个名字的同一类型的照样可以实现访问

1
2
3
4
5
6
7
typealias Username = String

fun validate(name: Username) {
    if(name.length < 5) {
        println("Username $name is too short.")
    }
}

就像下面这样, 如果我们在username传入了一个Password, 编译依旧会照常通过, :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
typealias Username = String
typealias Password = String

fun authenticate(user: Username, pass: Password) { /* ... */ }

fun main(args: Array<String>) {
    val username: Username = "user's pass"
    val password: Password = "user"
    authenticate(password, username)
}

但如果我们换成了 inline, 则会直接在我们写代码的时候爆红, 虽然他们本质上都会变成 String, 但是这样提高了我们的可扩展性以及安全性!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
inline class Username(val value: String)
inline class Password(val value: String)

fun authenticate(user: Username, pass: Password) { /* ... */ }

fun main(args: Array<String>) {
    val username: Username = Username("joe.user")
    val password: Password = Password("super-secret")
    authenticate(password, username) // <--- Compiler error here! =)
}

value Modifier

介绍

这个关键词从 kotlin 1.4.30 引入, 我发现它是因为我偶然写代码的时候发现敲完 va 之后除了 val var 两个修饰符以外还有一个 value 修饰符! 于是赶紧去翻了官方文档看这玩意怎么用, 终于在 kotlin 1.4.30 的更新日志中发现了这个新关键字, 同时注意到官方说这个修饰符还处于 beta 阶段, 于是我下载了 kotlin 1.5-M1 去提前体验一些新特性

用法

1
2
3
4
@JvmInline
value class Hour(private val v: Int) {
    fun toMinutes() = Minute(v * 60)
}

这个@JvmInline其实只是限定了 value 修饰符只能用在 JVM平台, 具体有什么功能呢?

其实只是多了一个可以在 inline class 中写 init 罢了,需要注意的是 value 修饰符注解的类即是一个增强版的 inline, 您无需再写 inline 修饰符:

1
2
3
4
5
6
7
@JvmInline
value class Hour(private val v: Int) {
    init {
        println(v)
    }
    fun toMinutes() = Minute(v * 60)
}

这里插一句, 既然这玩意在 JVM 平台, 那么它理应能与 Java 进行互操作, 事实上也是这样的! 我们只需要使用 @JvmName 注解即可, 这个注解的主要用途就是告诉编译器生成的 Java 类或者方法的名称, 当然这只是可以这么干, 我觉得新写的代码你还用 Java 去调用 kotlin 的代码, 属实不优雅, 纯 kotlin 才是正道hhhhh

Benchmark

class 与 inline class 的性能相差了较为明显的数量级, 因为前者创建了6亿个对象, 而后者采用了内联的方式减少了很多对象的创建

class inline class
https://cdn.jsdelivr.net/gh/zsqw123/cdn@master/picCDN/20210324010139.png https://cdn.jsdelivr.net/gh/zsqw123/cdn@master/picCDN/20210324010055.png

ref:

code: learn-kt/210323.kt at master · zsqw123/learn-kt (github.com)