看到Kotlin里满屏的 inline,我真的想 Java 了
❝多年以后,当我面对 Kotlin 源码满屏的inline/crossinline/noinline时,将会想起用 Eclipse 手动创建Java匿名内部类的那个遥远的下午。
❞
一切要用Java和匿名内部类讲起。
一、Java 与匿名内部类
Java 中最开始要使用高阶函数,需要先定义一个接口,比如Android里常用的 OnClickListener:
public interface OnClickListener {voidonClick(View v);}
然后使用的时候,需要创建一个实现这个接口的匿名内部类,这就是最原始的类似高阶函数的写法:
view.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {System.out.println("Hello World");}});
从 Java 8 开始,符合函数式接口规范(只包含一个抽象方法)的接口,可以用 Lambda 表达式简写成这样:
view.setOnClickListener(v -> System.out.println("Hello World"));这下代码少了不少,看起来终于“函数式”了一点。然而使用时需要先创建接口,虽然有这个特性,但是用的人也不多,在当时已经算是进阶用法了。
二、Kotlin 与高阶函数
后来有了 Kotlin ,Kotlin最大的优点之一就是原生支持 Lambda 和高阶函数了。Kotlin 并不需要提前定义接口,只要在函数参数中写函数类型就行了:
funwrapper(block: () -> Unit) {block()}
调用方式:
wrapper {println("Inside Block")}
看上去非常函数式,一个类也没写,但实际上……
查看 Kotlin 字节码(Tools -> Kotlin -> Show Kotlin Bytecode),可以看到下面的内容:
public final static wrapper(Lkotlin/jvm/functions/Function0;)V// annotable parameter count: 1 (invisible)@Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0L0ALOAD 0LDC "block"INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)VL1LINENUMBER 3 L1ALOAD 0INVOKEINTERFACE kotlin/jvm/functions/Function0.invoke ()Ljava/lang/Object; (itf)POPL2LINENUMBER 5 L2RETURNL3LOCALVARIABLE block Lkotlin/jvm/functions/Function0; L0 L3 0MAXSTACK = 2MAXLOCALS = 1
虽然一堆字节码看起来让人头晕,但是其实认得几个关键词就够用了:wrapper、Function0、Ojbect、invoke。
原来,lambda函数 wrapper中的函数类型参数block,被编译成了 Function0 类型的参数,并通过 invoke() 方法调用它。那Function0是什么呢?Function0 是 Kotlin 中预先定义好的函数接口,表示函数接收0个参数,而接收一个参数的则使用Function1,两个参数使用Function2,以此类推。源码在Functions.kt,如下:
// Functions.ktpublic interface Function0<out R> : Function<R> {public operator funinvoke(): R}public interface Function1<in P1, out R> : Function<R> {public operator funinvoke(p1: P1): R}...public interface Function22......
❝小问题:Functions.kt只定义到了Function22,那超过22个参数的函数怎么办?
❞
所以,实际上,Kotlin跟Java的Lambda基本没区别?都是使用接口和匿名内部类的形式。
但是如果是要创建类的话,那或多或少会有一些性能开销,有些高阶函数可能会被频繁调用。
三、inline 的作用:避免对象和调用开销
这个时候,Kotlin 提供了 inline 关键字。加上 inline 后,编译器会将函数和它的 lambda 参数的代码,在调用处直接展开,从而避免函数调用和对象创建。
示例:
// 加上inline关键字inline funwrapper(block: () -> Unit) {println("Start")block()println("End")}
调用:
wrapper {println("Inside Block")}
这段代码编译后,大致会变成:
println("Start")println("Inside Block")println("End")
❝可以打开 Kotlin Bytecode 面板,切换“Inline”选项查看区别。
❞
代码展开了,性能更好了,也没生成对象,非常的清晰明了。
如果一切就止步于此,我也只需要学一个关键字,但显然没这么简单...
四、inline 带来的非局部 return 行为
当 inline 函数中的 lambda 含有 return 时,会出现一些有点“反直觉”的行为。
比如下面的代码:
// 定义inline funwrapper(block: () -> Unit) {println("Start")block()println("End")}// 调用funtest() {wrapper {println("Inside")return}println("Outside")}
按常规理解会打印三行日志:“End”不输出,“Outside”能输出,但实际输出的结果是:
StartInside
也就是说调用者“Outside”也没有输出:“难道这个 return 结束了外部的函数?"
没错,这个 return 实际上直接跳出了 test() 函数,这也是 inline 带来的特性之一:
「非局部返回:return会跳出外部函数」
由于使用了 inline,block 的内容被直接展开,return也会被展开,相当于变成这样:
funtest() {println("Start")println("Inside")returnprintln("End") // 不执行println("Outside") // 不执行}
五、crossinline:阻止非局部返回
好吧,inline是将函数直接展开,相当于复制粘贴到这里,return就返回调用者了,也算合理。
但是假如「这个函数将在异步异步线程中调用呢」?例如:
inline funrunAsync(block: () -> Unit) {thread {block()}}
调用:
funmain() {runAsync {println("Inside thread")return}}
假设inline后展开:
funmain() {thread {println("Inside thread")return}}
思考一下,这样会有什么问题:main() 执行完就返回了,而线程在另一个栈异步执行,按照上文inline中return的逻辑,这个这个 return 会跳出 main()。 但是问题就出现了:main()已经结束了,这个时候再返回main就会引起异常。
所以这种情况是不被允许的,block()在编译器中会直接报下面的错误:
// 此处无法内联“block: () -> Unit”:它可能包含非局部返回。// 请将 “crossinline” 修饰符添加到参数声明 “block: () -> Unit”。Cannot inline 'block: () -> Unit' here: it might contain non-local returns.Add 'crossinline' modifier to parameter declaration 'block: () -> Unit'.
提示我们需要加上crossinline修饰符:
inline funrunAsync(crossinline block: () -> Unit) {thread {block()}}
之后,编译器会阻止lambda 中的 return 非局部返回,只能使用return@xxx的局部返回。
六、noinline:不内联
在 inline 函数中,所有 lambda 参数默认都会被内联。但有时候可能想保留 lambda 作为对象使用。
例如:
inline funwrapper(block: () -> Unit) {val b = block // ❌ 报错b()}
编译器会报错,因为试图把一个 inline 的参数当作值存到变量里。
为什么会报错?让我们从最开始思考:
❝❞
lambda会创建函数对象而增加性能消耗inline为了优化性能而将函数直接展开,不创建函数对象现在需要把函数当成变量传递,所以又需要函数对象
那到底是不需要对象优化性能,还是需要对象进行传递?编译器也蒙圈了,这个地方构成了语义冲突,所以这个被当成变量使用的函数,就无法inline了。这个时候可以对这个参数加上 noinline,告诉编译器这个参数不内联展开:
inline funwrapper(noinline block: () -> Unit) {val b = block // ✅ 编译通过b()}
多个参数的场景(有的函数参数要展开,而有的函数参数要当成变量不能展开):
inline funrun(block1: () -> Unit, noinline block2: () -> Unit) {val task = block2 // 可以编译task()block1()}
七、总结
inline 系列关键字的行为对比如下:
inline | |||
crossinline | |||
noinline |
Kotlin 的 lambda 也会生成函数对象,所以会带来一定的性能开销。 inline可以将函数在调用处展开,减少对象创建,同时如果写了return,也会一起展开,导致跳出调用者。在线程或协程中, return会造成跳出栈帧的语义错误,所以编译器不允许,使用crossinline可以让编译器阻止非局部return。inline是为了避免生成函数对象,而把 lambda 存到变量里传来传去又需要函数对象,造成了冲突,这种要使用noinline。如果搞不懂,可以不用。
思考
为什么Koltin要搞非局部返回?
❝没有非局部 return,代码逻辑一样能写,但「表达力会变差,控制流会变复杂」,尤其是在「组合式 API、DSL、协程」这些地方。(参考AI的理解)
❞
理想写法(有非局部 return)
inline fundoIf(condition: Boolean, block: () -> Unit) {if (condition) block()}funlogin(user: String?) {doIf(user == null) {println("no user")return // ✅ 非局部 return:直接跳出 login()}println("login user: $user")}
这段代码可读性非常高,像是在自然叙述:
❝如果用户是 null,就执行块并跳出函数;否则继续执行。
❞
如果没有非局部 return,会怎么样?
那 block 就不能直接 return 出 login(),它只能跳出 lambda 本身。所以这段代码必须“手动补救”,如下面这种加flag的写法:
funlogin(user: String?) {var shouldReturn = falsedoIf(user == null) {println("no user")shouldReturn = true}if (shouldReturn) returnprintln("login user: $user")}