零、return 的含义
在 Java 中, return 用于场景:
- 用于普通方法中
- 用于 lambda 中
对于普通方法当然是 return 到方法调用处,对于 lambda 则是 return 到 lambda 的调用处。
但是在 Kotlin 中, return 的裸返回(非标签返回)就只有一个处理:总是从最内层的函数中返回,lambda 中的 return 也是返回到调用 lambda 的函数处,而不是 lambda 的调用处。
之所以在 Kotlin 中不再区分 lambda,是因为 lambda 表达式在 Kotlin 函数中作为最后一个参数时, lambda 表达式可以只有大括号,并且写在函数的小括号外部,这时在语言结构上,它就像极了普通函数的结构。这时如果还区分 return 在 lambda 中的不同行为,这样就会导致同一个语言结构, 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 表达式非常灵活,既支持「裸返回」也支持「标签返回」,而且这两种形式在意义上是不一样的,原因就是「零」中提到的裸
return在 Kotlin 中只有一个意义:返回到最内层的函数。内联 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
}
}
除非, innerFun 是 inline 函数:
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) {}