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。
作者:巴菲猫
原文链接:https://www.jianshu.com/p/76ba9c2af426