Kotlin inline, noinline and crossinline
tags: Kotlin inline, noinline, crossinline
简介
kotlin 中,有三个类似的概念,inline
,noinline
和 crossinline
。平时使用的时候,很容易混淆。本文会介绍这三个概念的用法以及区别。
inline
inline
就是我们常说的内联。这个关键字会在编译期间起作用。如果一个函数是 inline
的,那么编译器会在编译的时候,把这个函数复制到调用处。这样做有什么好处呢?总的来说,好处有三个:
第一点,会减少函数调用的次数。我们知道,虽然函数调用的开销很小,但是确实是有一定的开销的。尤其是在大量的循环中,这种开销会变得更加明显。
比如如下代码:
// Kotlin
fun main(args: Array<String>) {
multiplyByTwo(5)
}
fun multiplyByTwo(num: Int) : Int {
return num * 2
}
他进行反编译之后的等价 Java 代码如下:
// Java
public static final void main(@NotNull String[] args) {
//...
multiplyByTwo(5);
}
public static final int multiplyByTwo(int num) {
return num * 2;
}
可以看到,不加 inline
的方法,编译成字节码,然后再反编译成等价 java 代码,得到的结果是一个普通的方法。这个跟我们的常识是吻合的。
但是,当我们把方法用 inline
修饰了之后,会发生什么呢?
比如如下代码中,我们把 multiplyByTwo
用 inline
参数修饰了一下:
// Kotlin
fun main(args: Array<String>) {
multiplyByTwo(5)
}
inline fun multiplyByTwo(num: Int) : Int {
return num * 2
}
反编译得到的结果如下:
// Java
public static final void main(@NotNull String[] args) {
// ...
int num$iv = 5;
int var10000 = num$iv * 2;
}
public static final int multiplyByTwo(int num) {
return num * 2;
}
可以看到,inline 中的方法,被复制到了调用方。这就是 inline 威力强大的地方!
第二点,会减少对象的生成。当方法中,有一个参数是 lambda
的时候,使用 inline
的方法,可以减少对象的生成。kotlin 对于默认的 lambda
参数的处理方式为,把 lambda
转化成一个类,看起来跟 java 中的匿名内部类非常相似。
比如,
// Kotlin
fun main(args: Array<String>) {
val methodName = "main"
multiplyByTwo(5) { result: Int -> println("call method $methodName, Result is: $result") }
}
fun multiplyByTwo(num: Int,
lambda: (result: Int) -> Unit): Int {
val result = num * 2
lambda.invoke(result)
return result
}
反编译之后的结果有点复杂:
public final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
final String methodName = "main";
this.multiplyByTwo(5, (Function1)(new Function1() {
public Object invoke(Object var1) {
this.invoke(((Number)var1).intValue());
return Unit.INSTANCE;
}
public final void invoke(int result) {
String var2 = "call method " + methodName + ", Result is: " + result;
boolean var3 = false;
System.out.println(var2);
}
}));
}
public final int multiplyByTwo(int num, @NotNull Function1 lambda) {
Intrinsics.checkParameterIsNotNull(lambda, "lambda");
int result = num * 2;
lambda.invoke(result);
return result;
}
观察生成的结果:java 生成了一个 Function1
类型的对象,来表示这个 lambda
。其中,Funtion1
中的 1 就代表这个 lambda 值需要一个参数。类似的,如果是不需要参数的,那么就是 Function0
。这个生成的结果,跟我们平时写 java 代码的时候使用的匿名内部类的方式是一样的。那么,可想而知,如果这个 lambda
是在一个循环中被调用的,那么就会生成大量的对象。
既然,inline
有如上的好处,那么是否有什么“坏处”,或者会造成我们使用不方便的地方呢?
首先是,对于一个 public
的 inline
方法,他不可以引用类的私有变量。比如:
private val happy = true
inline fun testNonPrivateField() {
println("happy = ${happy}")
}
如果这么写代码,编译器会对 happy 报错。道理也很简单:既然 inline 是在编译期间复制到调用方,那么自然就不能引用类的私有变量,因为调用方很大可能应该是“看不见”这个私有变量的。
其次,inline 方法会对流程造成非常隐晦的影响。
// Kotlin
fun main(args: Array<String>) {
println("Start of main")
multiplyByTwo(5) {
println("Result is: $it")
return
}
println("End of main")
}
// Java
public static final void main(@NotNull String[] args) {
String var1 = "Start of main";
System.out.println(var1);
int num$iv = 5;
int result$iv = num$iv * 2;
String var4 = "Result is: " + result$iv;
System.out.println(var4);
}
观察上面的两端代码,我们发现在反编译出来的 java 代码中,没有找到 “End of main”。为什么呢?原因其实很简单:根据我们前面知道的,inline
其实就是把代码在编译期间复制到调用方,因此,如果 lambda 中有 return 语句,那么也会被原样复制过去,进而,因为 lambda
中的 return
的影响,导致编译器认为后面的 “End of main” 其实是不能被访问到的代码,于是在编译期间给去掉了。
所以,小结一下:inline
关键字的作用,是把 inline
方法以及方法中的 lambda
参数在编译期间复制到调用方,进而减少函数调用以及对象生成。
不过,inline
关键字对于 lambda
的处理有的时候不是我们想要的。也就是,有时我们不想让 lambda
也被 inline
。那么有什么办法呢?这个时候就需要 noinline
关键字了。
noinline
noinline
修饰的是 inline
方法中的 lambda
参数。noinline
用于我们不想让 inline
特性作用到 inline
方法的某些 lambda
参数上的场景。
比如:
// Kotlin
fun main(args: Array<String>) {
val methodName = "main"
multiplyByTwo(5) {
result: Int -> println("call method $methodName, Result is: $result")
}
}
inline fun multiplyByTwo(
num: Int,
noinline lambda: (result: Int) -> Unit): Int {
val result = num * 2
lambda.invoke(result)
return result
}
反编译的结果是:
public final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
final String methodName = "main";
byte num$iv = 5;
Function1 lambda$iv = (Function1)(new Function1() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke(Object var1) {
this.invoke(((Number)var1).intValue());
return Unit.INSTANCE;
}
public final void invoke(int result) {
String var2 = "call method " + methodName + ", Result is: " + result;
boolean var3 = false;
System.out.println(var2);
}
});
int $i$f$multiplyByTwo = false;
int result$iv = num$iv * 2;
lambda$iv.invoke(result$iv);
}
public final int multiplyByTwo(int num, @NotNull Function1 lambda) {
int $i$f$multiplyByTwo = 0;
Intrinsics.checkParameterIsNotNull(lambda, "lambda");
int result = num * 2;
lambda.invoke(result);
return result;
}
可以看到, 因为使用了 noinline
修饰了 lambda
,所以,编译器使用了匿名内部类的方式来处理这个 lambda,生成了一个 Function1
对象。
crossinline
是不是有了 inline
和 noinline
,对于我们开发人员来讲就够了呢?就满足了呢?显然不是的。考虑一种情况,我们既想让 lambda
也被 inline
,但是又不想让 lambda
对调用方的控制流程产生影响。这个产生影响,可以是有意识的主动控制,但是大多数情况下是开发人员的不小心导致的。我们知道 java 语言是一个编译型语言,如果能在编译期间对这种 inline
lambda
对调用方产生控制流程影响的地方进行提示甚至报错,就万无一失了。
crossinline
就是为了处理这种情况而产生的。crossinline
保留了 inline
特性,但是如果想在传入的 lambda
里面 return
的话,就会报错。return
只能 return
当前的这个 lambda
。
// Kotlin
fun main(args: Array<String>) {
val methodName = "main"
multiplyByTwo(5) {
result: Int -> println("call method $methodName, Result is: $result")
return@multiplyByTwo
}
}
如面代码所示,必须 return@multiplyByTwo
,而不能直接写 return
。
总结
inline
关键字的作用,是把 inline
方法以及方法中的 lambda
参数在编译期间复制到调用方,进而减少函数调用以及对象生成。对于有时候我们不想让 inline
关键字对 lambda
参数产生影响,可以使用 noline
关键字。如果想 lambda
也被 inline
,但是不影响调用方的控制流程,那么就要是用 crossinline
。