看以下 forEach() 高阶函数的调用:
//Kotlin
//CodeSegment1
var ints = intArrayOf(1, 2, 3, 4)
fun main() {
ints.forEach {
println(it)
}
}
复制代码
在 main() 函数内调用了 ints 的 forEach() 高阶函数,正常一个函数的调用过程是一个 压栈、调用执行、出栈 的过程,如果被调用函数执行内容过于简单,例如这里的 println(it) ,相对于调用执行的开销,压栈和出栈的开销将会过大。为了解决这个问题, 内联函数 的概念出现了。
内联函数在Kotlin中用 inline 关键字标识,在内联函数被调用的时候,编译器直接将直接把函数的执行内容“插入”到调用函数的位置,以避免压栈和出栈的开销。
我们看一下 IntArray 的 forEach() 的定义:
//Kotlin
public inline fun kotlin.IntArray.forEach(action: (kotlin.Int) -> kotlin.Unit): kotlin.Unit { /* compiled code */ }
复制代码
因为有 inline 关键字,所以编译器在执行CodeSegment1时,完全等价于以下代码:
//Kotlin
//CodeSegment2
var ints = intArrayOf(1,2,3,4)
fun main() {
for (i in ints) {
println(i)
}
}
复制代码
避免了调用函数带来的压栈、出栈开销。
定义函数时,在前面添加一个 inline 关键字,该函数就被定义成内联函数。
注意!并非所有函数都适合定义成内联函数,需要看函数执行的开销,往往函数执行开销越小,调用函数的压栈和出栈开销越“不划算”,越应该定义成内联函数。
高阶函数要么传入参数包含函数类型,要么输出结果是函数类型,所以高阶函数直接调用产生的压栈、出栈开销将比较大,所以一般都会定义成内联函数。
定义一个计算代码块执行耗时的高阶函数 cost(block: ()->Unit) :
//Kotlin
inline fun cost(block: () -> Unit) {
val start: Long = System.currentTimeMillis()
block()
println("${System.currentTimeMillis() - start} ms")
}
复制代码
当我们在 main() 函数中调用 cost() 函数时:
//Kotlin
fun main() {
cost { println("Hello inline function") }
}
复制代码
编译器实际执行的代码相当于:
//Kotlin
fun main() {
val start: Long = System.currentTimeMillis()
println("Hello inline function")
println("${System.currentTimeMillis() - start} ms")
}
复制代码
这里发生了 两次内联 :
设想这样一个场景,现在需要打印CodeSegment1中的 ints ,但是,当遇到 3 时,不打印,如何在 forEach() 中实现这一点呢?操作如下:
//Kotlin
var ints = intArrayOf(1, 2, 3, 4)
fun main() {
ints.forEach {
if (it == 3) return@forEach
println(it)
}
println("Dividing")
ints.forEach {
if (it == 3) return
println(it)
}
println("Ending")
}
复制代码
控制台打印:
return@forEach 和 return 的打印结果完全不同。
return@forEach 将提前结束本次循环,相当于 continue ; return 将结束所在函数,此处即为 main() 函数,因为 Ending 没有打印。 像 return@forEach 这样的返回称为 local return 。
上面的 return 就是 non-local return 。
看这样一个例子,如果将高阶函数定义成 内联函数 :
//Kotlin
inline fun nonLocalReturn(block: ()->Unit){
block()
}
fun main() {
println("Starting")
nonLocalReturn { return }
println("Ending")
}
复制代码
控制台将打印:
如果将高阶函数定义成 非内联函数 :
则只能执行local return。
对于内联函数,因为没有压栈和出栈操作,所以直接 return 对内联函数无效,效果将出现在包裹内联函数的函数,这里是 main() 函数。
看这样一个例子:
//Kotlin
inline fun func1(block:()->Unit){
println("func1 starting")
block()
println("func1 ending")
}
inline fun func2(){
println("func2 starting")
return
println("func2 ending")
}
fun main() {
func1 {
func2()
}
}
复制代码
Q: func1 ending 会不会打印?
A:会打印:
按理说, func2() 也没有压栈操作, return 操作对其应该无效,同样的,包裹 func2() 的 func1() 也没有压栈操作, return 对 func1() 也无效,这个 return 应该直接使得 main() 函数弹出栈,不打印 func1 ending 才对?
字节码反编译如下:
//Java
package imooc.chapter_6.inline;
import kotlin.Metadata;
import kotlin.jvm.functions.Function0;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;
@Metadata(
mv = {1, 1, 16},
bv = {1, 0, 3},
k = 2,
d1 = {"/u0000/u0010/n/u0000/n/u0002/u0010/u0002/n/u0000/n/u0002/u0018/u0002/n/u0002/b/u0003/u001a/u0017/u0010/u0000/u001a/u00020/u00012/f/u0010/u0002/u001a/b/u0012/u0004/u0012/u00020/u00010/u0003H/u0086/b/u001a/t/u0010/u0004/u001a/u00020/u0001H/u0086/b/u001a/u0006/u0010/u0005/u001a/u00020/u0001¨/u0006/u0006"},
d2 = {"func1", "", "block", "Lkotlin/Function0;", "func2", "main", "LearnKotlin.main"}
)
public final class Inline_funcKt {
public static final void func1(@NotNull Function0 block) {
int $i$f$func1 = 0;
Intrinsics.checkParameterIsNotNull(block, "block");
String var2 = "func1 starting";
boolean var3 = false;
System.out.println(var2);
block.invoke();
var2 = "func1 ending";
var3 = false;
System.out.println(var2);
}
public static final void func2() {
int $i$f$func2 = 0;
String var1 = "func2 starting";
boolean var2 = false;
System.out.println(var1);
}
public static final void main() {
int $i$f$func1 = false;
String var1 = "func1 starting";
boolean var2 = false;
System.out.println(var1);
int var3 = false;
int $i$f$func2 = false;
String var5 = "func2 starting";
boolean var6 = false;
System.out.println(var5);
var1 = "func1 ending";
var2 = false;
System.out.println(var1);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
复制代码
怎么解释?
添加 crossinline 关键字,禁止 non-local return :
添加 noinline 关键字禁止函数参数内联:
这样的情况下,高阶函数前面的 inline 关键字将是多余的:
没有back-field的属性的getter/setter可以设置内联;
public/protected 的内联函数只能访问对应类的 public 成员;