看以下 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
成员;