刨根问底:为什么kotlin中,不对val声明的局部变量做常量优化?

666 阅读3分钟

知识点

首先,我们看一下几个点:

  1. 常量:在计算机程序运行时,不会被程序修改的量
  2. 常量优化:编译优化中的一种手段,在保持语义不变的情况下提高程序运行效率。常量折叠,又称常量传播 (Constant Propagation),如果一个表达式可以确定为常量,在他的下一个定义 (Definition) 前,可以进行常量传播

与Java做对比

在Java中,局部变量尽量声明为常量,也是当初背面试题的老八股了。基于上面的知识点,写两个例子对比一下。

Java例子

image.png

右边是编译后重新反编译的代码(大家也可以用jclasslib插件直接看class字节码,自行解读后也是一样的)。 可以发现,用final修饰的变量在使用处进行了直接的替换,相比没用final修饰,运行效率确实会有所提升。

kotlin例子

image.png

可以看到,变量d在编译时直接优化成了4,但对于变量c并没有做优化。

继续尝试

在Java里有final关键字来定义常量,kotlin中也有const关键字。查阅相关资料/文章,其实用这关键字修饰的变量叫做“常变量”,和直接定义写死的“符号常量”还是有所区别。所以上述kotlin例子中,还是有做常量优化的,那么现在继续用const关键字来试试。

image.png

可以看到,在testConst函数中,此时c直接等于4,也是做了相关优化的。

思考

kotlin中,val也代表不可变,其在文档中解释如下:Use the val keyword to declare variables that are assigned a value only once. These are immutable, read-only local variables that can’t be reassigned a different value after initialization

虽然也是不可变,但和“常量”还是有所区别的,比如对于成员变量,虽然用val声明,但也可以对其值通过反射进行修改。

image.png

如图所示,mem是Test中的一个val声明的成员变量,甚至通过字节码也可以看到,这个mem是final的,但是,就算这样,我们还是可以通过反射修改其值。

image.png

本质还是因为我们实际是通过这个getMem方法获取mem的值的。

而如果在Java中,我们同样的写出这样的代码,这个getMem方法将会直接返回123(常量折叠),所以就算反射修改了mem的值,也不影响。

而且,kotlin中可以将一个表达式赋值给变量,那么就可以通过不同表达式,不同条件初始化一个值。

所以的确不能说,不能把val当作常量。

但是,对于在方法中的局部变量(讨论基本数据类型),如果用val声明,并且“有确定初始值”的变量,我认为是完全没有这些差异,也不可能对局部变量做反射操作,比如在一个方法中val a = 1。这个a是可以当作常量的,在使用到a的地方也完全可以通过编译时常量优化对其进行转换。

那么为什么在编译时,不对局部变量中,使用val声明的并且“有确定初始值”的变量做常量优化的编译优化操作呢?

又或者,为什么const关键字不能在局部变量中声明呢(Modifier 'const' is not applicable to 'local variable')?

进阶

如果说官方没对这个做处理,怕打破val关键字的意义(毕竟这确实不是常量的概念),那么我们自己来做处理。目前想到的方案就是通过KCP(kotlin编译器插件)来处理,通过自行判断并转换编译后端的IR节点,来达到将val声明的并且“有确定初始值”的变量转换成常量的操作。具体请看我下一篇文章,有详细介绍和步骤--通过KCP实现无为转变 - 掘金 (juejin.cn)