看到Kotlin里满屏的 inline,我真的想 Java 了

7,312 阅读7分钟

多年以后,当我面对 Kotlin 源码满屏的inline/crossinline/noinline时,将会想起用 Eclipse 手动创建Java匿名内部类的那个遥远的下午。

一切要用Java和匿名内部类讲起。

一、Java 与匿名内部类

Java 中最开始要使用高阶函数,需要先定义一个接口,比如Android里常用的 OnClickListener:

public interface OnClickListener {
    void onClick(View v);
}

然后使用的时候,需要创建一个实现这个接口的匿名内部类,这就是最原始的类似高阶函数的写法:

view.setOnClickListener(new View.OnClickListener() {
    @Override
    public 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 并不需要提前定义接口,只要在函数参数中写函数类型就行了:

fun wrapper(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 0
   L0
    ALOAD 0
    LDC "block"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
   L1
    LINENUMBER 3 L1
    ALOAD 0
    INVOKEINTERFACE kotlin/jvm/functions/Function0.invoke ()Ljava/lang/Object; (itf)
    POP
   L2
    LINENUMBER 5 L2
    RETURN
   L3
    LOCALVARIABLE block Lkotlin/jvm/functions/Function0; L0 L3 0
    MAXSTACK = 2
    MAXLOCALS = 1

虽然一堆字节码看起来让人头晕,但是其实认得几个关键词就够用了:wrapper、Function0、Ojbect、invoke。
原来,lambda函数 wrapper中的函数类型参数block,被编译成了 Function0 类型的参数,并通过 invoke() 方法调用它。那Function0是什么呢?
Function0 是 Kotlin 中预先定义好的函数接口,表示函数接收0个参数,而接收一个参数的则使用Function1,两个参数使用Function2,以此类推。源码在Functions.kt,如下:

// Functions.kt
public interface Function0<out R> : Function<R> {
    public operator fun invoke(): R
}
public interface Function1<in P1, out R> : Function<R> {
    public operator fun invoke(p1: P1): R
}
...
public interface Function22...
...

小问题:Functions.kt只定义到了Function22,那超过22个参数的函数怎么办?

所以,实际上,Kotlin跟Java的Lambda基本没区别?都是使用接口和匿名内部类的形式。
但是如果是要创建类的话,那或多或少会有一些性能开销,有些高阶函数可能会被频繁调用。

三、inline 的作用:避免对象和调用开销

这个时候,Kotlin 提供了 inline 关键字。加上 inline 后,编译器会将函数和它的 lambda 参数的代码,在调用处直接展开,从而避免函数调用和对象创建。

示例:

// 加上inline关键字
inline fun wrapper(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 fun wrapper(block: () -> Unit) {
    println("Start")
    block()
    println("End")
}
// 调用
fun test() {
    wrapper {
        println("Inside")
        return
    }
    println("Outside")
}

按常规理解会打印三行日志:“End”不输出,“Outside”能输出,但实际输出的结果是:

Start  
Inside

也就是说调用者“Outside”也没有输出:“难道这个 return 结束了外部的函数?"
没错,这个 return 实际上直接跳出了 test() 函数,这也是 inline 带来的特性之一:
非局部返回:return会跳出外部函数
由于使用了 inlineblock 的内容被直接展开,return也会被展开,相当于变成这样:

fun test() {
    println("Start")
    println("Inside")
    return
    println("End")     // 不执行
    println("Outside") // 不执行
}

五、crossinline:阻止非局部返回

好吧,inline是将函数直接展开,相当于复制粘贴到这里,return就返回调用者了,也算合理。
但是假如这个函数将在异步异步线程中调用呢?例如:

inline fun runAsync(block: () -> Unit) {
    thread {
        block()
    }
}

调用:

fun main() {
    runAsync {
        println("Inside thread")
        return
    }
}

假设inline后展开:

fun main() {
    thread {
        println("Inside thread")
        return
    }
}

思考一下,这样会有什么问题:main() 执行完就返回了,而线程在另一个栈异步执行,按照上文inlinereturn的逻辑,这个这个 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 fun runAsync(crossinline block: () -> Unit) {
    thread {
        block()
    }
}

之后,编译器会阻止lambda 中的 return 非局部返回,只能使用return@xxx的局部返回。

六、noinline:不内联

在 inline 函数中,所有 lambda 参数默认都会被内联。但有时候可能想保留 lambda 作为对象使用。
例如:

inline fun wrapper(block: () -> Unit) {
    val b = block  // ❌ 报错
    b()
}

编译器会报错,因为试图把一个 inline 的参数当作值存到变量里。
为什么会报错?让我们从最开始思考:

  • lambda 会创建函数对象而增加性能消耗
  • inline 为了优化性能而将函数直接展开,不创建函数对象
  • 现在需要把函数当成变量传递,所以又需要函数对象

那到底是不需要对象优化性能,还是需要对象进行传递?编译器也蒙圈了,这个地方构成了语义冲突,所以这个被当成变量使用的函数,就无法inline了。这个时候可以对这个参数加上 noinline,告诉编译器这个参数不内联展开:

inline fun wrapper(noinline block: () -> Unit) {
    val b = block  // ✅ 编译通过
    b()
}

多个参数的场景(有的函数参数要展开,而有的函数参数要当成变量不能展开):

inline fun run(block1: () -> Unit, noinline block2: () -> Unit) {
    val task = block2  // 可以编译
    task()
    block1()
}

七、总结

inline 系列关键字的行为对比如下:

修饰符是否内联是否允许非局部 return是否可以保存/传递 lambda
inline
crossinline
noinline
  1. Kotlin 的 lambda 也会生成函数对象,所以会带来一定的性能开销。inline 可以将函数在调用处展开,减少对象创建,同时如果写了 return,也会一起展开,导致跳出调用者。
  2. 在线程或协程中,return 会造成跳出栈帧的语义错误,所以编译器不允许,使用 crossinline 可以让编译器阻止非局部 return
  3. inline 是为了避免生成函数对象,而把 lambda 存到变量里传来传去又需要函数对象,造成了冲突,这种要使用 noinline
  4. 如果搞不懂,可以不用。

思考

为什么Koltin要搞非局部返回?

没有非局部 return,代码逻辑一样能写,但表达力会变差,控制流会变复杂,尤其是在组合式 API、DSL、协程这些地方。(参考AI的理解)

  • 理想写法(有非局部 return)
inline fun doIf(condition: Boolean, block: () -> Unit) {
    if (condition) block()
}

fun login(user: String?) {
    doIf(user == null) {
        println("no user")
        return  // ✅ 非局部 return:直接跳出 login()
    }

    println("login user: $user")
}

这段代码可读性非常高,像是在自然叙述:

如果用户是 null,就执行块并跳出函数;否则继续执行。

  • 如果没有非局部 return,会怎么样?

block 就不能直接 return 出 login(),它只能跳出 lambda 本身。所以这段代码必须“手动补救”,如下面这种加flag的写法:

fun login(user: String?) {
    var shouldReturn = false

    doIf(user == null) {
        println("no user")
        shouldReturn = true
    }

    if (shouldReturn) return

    println("login user: $user")
}