你真的了解 Kotlin 内联吗:标签返回、非局部返回、noinline、crossinline

693 阅读8分钟

零、return 的含义

Java 中, return 用于场景:

  1. 用于普通方法中
  2. 用于 lambda 中

对于普通方法当然是 return 到方法调用处,对于 lambda 则是 return 到 lambda 的调用处。

但是在 Kotlin 中, return 的裸返回(非标签返回)就只有一个处理:总是从最内层的函数中返回,lambda 中的 return 也是返回到调用 lambda 的函数处,而不是 lambda 的调用处。

之所以在 Kotlin 中不再区分 lambda,是因为 lambda 表达式在 Kotlin 函数中作为最后一个参数时, lambda 表达式可以只有大括号,并且写在函数的小括号外部,这时在语言结构上,它就像极了普通函数的结构。这时如果还区分 returnlambda 中的不同行为,这样就会导致同一个语言结构, return 却有不同的行为。因此 Kotlin 定义 return 总是从内层的函数中返回,即使在 lambda 中,它也会使调用 lambda 的函数返回,而不是让 lambda 返回。

这个只是 Kotlin 的原则,记住这个原则会更有助于理解下面的内联 lambda

我们时常在技术文章中看到「非局部返回」这样的术语,理解这个术语非常重要:

lambda 和 函数都是独立的调用层级,我们将不返回到本层级称为「非局部返回」。

一、裸返回 与 标签返回

1.1 普通函数

在普通函数中,即支持裸返回,也支持标签返回,默认标签就是函数名:

fun anonymousFun(): Int {
    return@anonymousFun 1
}

fun anonymousFun(): Int {
    return 1
}

1.2 lambda 表达式

lambda 表达式分为「内联」和「非内联」两种形式:

  • 内联的 lambda 表达式非常灵活,既支持「裸返回」也支持「标签返回」,而且这两种形式在意义上是不一样的,原因就是「零」中提到的裸 returnKotlin 中只有一个意义:返回到最内层的函数。内联 lambda 的「标签返回」在下面 1.2.2 再细讲。
  • 非内联 lambda 表达式则只支持「标签返回」,下面我们会看到本质原因是什么。

1.2.1 内联 lambda 的裸返回

我们先看内联 lambda 表达式的裸返回:

fun foo() {
    listOf(1, 2, 3, 4, 5).forEach {
        if (it == 3) return // 非局部返回:直接使 foo 函数返回了
        print(it)
    }
    println("函数执行不到这里")
}

12

由于 return 总是返回到最内层的函数调用处,因此上面例子会在遍历到 3 时,直接使 foo 函数返回。

💡 需要注意的是,这里的 forEach 是内联的,如果是非内联 lambda 则不再支持裸返回,下一节我们会看到为什么

但也正是因为内联 lambda 的裸返回是「非局部返回」,因此它不能使用到另一个 context(上下文) 中:

inline fun inlineFun(t: () -> Unit) {
    Thread(Runnable { t() }).start() // 这里调用 t() 会报错
}

fun testInline() {
    inlineFun {
        return // 这里可以「非局部返回」到 testInline 方法。由于这里调用时,IDE 并不知道 inlineFun 的实现中切换了上下文,因此这里不会报错
    }
}

内联 lambda t 由于是「非局部返回」,因此总是返回到包裹它的最内层函数处。根据「零」中的解释我们知道, Kotlin 其实是希望 return 的行为总是一致的,也就是返回到 testInline 处,但是上例中, t 却是返回到 Runnable 处。因此 Kotlin 是不允许在另一个上下文中调用内联 lambda 的。同样的原因引起的另一个问题,我们还会在 「1.2.3」中看到。

1.2.2 内联 lambda 的标签返回

对于「1.2.1」中的例子,如果我们只希望跳过 it == 3 这一次循环,也就是在 it == 3 时我们只希望返回到 forEach ,那么这时候我们就可以显式地通过「标签」指定 lambda 的返回处(这个「标签」需要定义在最内层函数的内层):

fun foo() {
    listOf(1, 2, 3, 4, 5).forEach lit@{
        if (it == 3) return@lit // 局部返回,返回到 lambda 调用处
        print(it)
    }
    print(" 使用显式标签后,可以执行到这里")
}

1245 使用显式标签后,可以执行到这里

上面我们使用的是显式标签,我们还可以使用隐式标签,标签名与 lambda 名字一样:

fun foo() {
    listOf(1, 2, 3, 4, 5).forEach {
        if (it == 3) return@forEach // 局部返回,返回到 lambda 调用处
        print(it)
    }
    print(" 使用隐式标签后,可以执行到这里")
}

1245 使用隐式标签后,可以执行到这里

如果你了解「函数类型」 :: Callable references 的话,你就会知道实际上 lambda 表达式是一种函数类型,因此我们可以为 forEach 传入一个函数实例就可以了。比如下面我们可以传入一个匿名函数:

fun foo() {
    listOf(1, 2, 3, 4, 5).forEach(fun(value: Int) {
        if (value == 3) return // 局部返回,返回到最内层的函数调用,也就是匿名函数处
        print(value)
    })
    print(" 使用匿名函数,可以执行到这里")
}

12 使用匿名函数,可以执行到这里

上面例子中的 return 都是实现了类似 continue 的功能,但是如果我们想实现一个 break 功能,就需要在外面再包一层 lambda

fun foo() {
    run loop@{
        listOf(1, 2, 3, 4, 5).forEach {
            if (it == 3) return@loop // 非局部返回,返回到 loop@ 标签处
            print(it)
        }
    }
    print(" 使用 loop 标签,可以执行到这里")
}

12 使用 loop 标签,可以执行到这里

1.2.3 非内联 lambda 不支持裸返回

在非内联 lambda 表达式中是不支持裸返回的。我们知道裸 return 是返回到最内层的函数调用处。但是非内联 lambda 可能被使用者保存后,在另一个函数中调用(甚至是异步线程),这时的裸 return 的返回处就存在了不确定性,因此是被禁止的:

fun foo(t: (Int) -> Unit) {
    Thread(Runnable { t(1) }).start() // t 可能被包装到一个 Runnable 中,此时它的最外层函数并不是 foo
}

fun test() {
    foo { return } // 这里 return 会报错
}

1.2.4 非内联 lambda 的标签返回

非内联 lambda 只支持标签返回:

fun foo(t: (Int) -> Unit) {
    Thread(Runnable {
        t(1)
        println("能够执行到这里")
    }).start()
}

fun test() {
    foo { return@foo } // 可以通过编译,返回到标签 foo
}

能够执行到这里

上面的标签 foo 并不是指的返回到 foo 函数,而是返回到 lambda 的调用处,只不过这个调用处有个隐式标签 foo

如果我们将标签换为 @test,则不能编译,因为这就等同于「裸返回」了。

💡 事实上,本质上就是:非内联 lambda 不支持「非局部返回」,必须通过显式的指定标签,实现「局部返回」。

二、非局部返回

有了前面的知识,我们就知道「非局部返回」只对内联的 lambda 表达式有效,它表示在 lambda 中直接返回包裹 lambda 的函数处。

在一个函数中,如果存在一个 lambda 表达式,在该 lambda 中不支持直接进行 return 退出该函数,比如:

fun innerFun(a: () -> Int) {} // 🔥 注意这里 innerFun 是一个非内联函数

fun outterFun() {
    innerFun {
        //return  //错误,不支持直接return
        //只支持通过标签,返回innerFun,实现局部返回
        return@innerFun 1
    }

    //如果是匿名或者具名函数,则支持
    var f = fun(){
        return
    }
}

除非, innerFuninline 函数:

inline fun innerFun(a: () -> Int) {} // 🔥 注意这里 innerFun 是一个 inline 函数,顺便也让 a 内联了

fun outterFun() {
    innerFun {
        return  //支持直接返回outterFun
        // return@innerFun 1 //如果要返回lambda,则必须有返回值
    }
}

这种直接在 lambda 返回外部函数的情况称为「非局部返回」。这里之所以能够「非局部返回」,不仅是因为 innerFun 是内联函数,还因为 inline 关键字让 a 也变为了内联的,因此在 a 中能够层层往外层返回

三、noinline

内联函数关键字 inline 的意义不仅让函数调用被内联,还会让函数参数被内联。比如某个内联函数,接收一个函数作为参数,那么这个作为参数的函数也会被内联。如果你不想函数参数被内联,可以使用关键字 noinline 修饰函数参数:

var notInlinedVar: (() -> Unit)? = null

inline fun noinlineFun(ignore: (Int) -> Unit, noinline notInlined: () -> Unit) {
    notInlinedVar = notInlined // noinline 修饰的 lambda 可以被存储,就像一个普通的函数变量一样
    foo(notInlined) // noinilne 修饰的 lambda 可以传递给接收非内联 lambda 参数的函数
    Thread(Runnable { notInlined() }).start() // noinlien 修饰的 lambda 可以在另一个 context 中调用
}

fun testNoinline() {
    noinlineFun({}) {
//      return // 这里不能使用「非局部返回」,因此报错
        return@noinlineFun
    }
}

fun foo(t: () -> Unit) {}

💡 可以内联的函数作为参数时,只能在内联函数内部调用或者作为可内联的参数传递。但是用 noinline 修饰的函数参数则可以像普通参数一样被存储、传递,因此在其中依然是不能裸 return 的(与非内联 lambda 一样,裸返回等价于「非局部返回」,而这是不支持的)。

四、crossinline

crossinline 的作用就是让被标记的内联 lambda 参数不能「非局部返回」(本来是可以「非局部返回」的),也就是说告诉编译器,这个函数参数的实现中,只能 return 到这个函数本身(局部返回),而不能 return 到函数外面的层级(非局部返回)。

但是这个 lambda 依然是内联的,也就是说它不能被存储,也不能传递给非内联参数:

var crossInlinedVar: (() -> Unit)? = null

inline fun crossinlineFun(ignore: (Int) -> Unit, crossinline crossinlined: () -> Unit) {
//    crossInlinedVar = crossinlined // 不能被存储
//    foo(crossinlined) // 不能传递到非内联 lambda 参数
    Thread(Runnable { crossinlined() }).start() // 由于不能「非局部返回」,因此可以在另一个 context 中调用
}

fun testCrossinline() {
    crossinlineFun({}) {
//      return // 由于 crossinline,因此不能「非局部返回」
        return@crossinlineFun
    }
}

fun foo(t: () -> Unit) {}