Kotlin 在 2.1.0 版本时,发布了一个 preview 级别的特性 Non-local break and continue。
英文名有点长,这里笔者简短的翻译成——跨域跳转。
该特性允许你使用非局部的 break 和 continue,扩展了你在内联函数范围内可以使用的工具集,并减少了项目中的样板代码。
这篇文章我们一起来探究下,跨域跳转 有何魔力。
开胃菜
fun main() {
val bobLoveFruits = arrayOf("Apple", "Banana", "Orange", "Pineapple", "Watermelon")
val jayLoveFruits = arrayOf("Apple", "Banana", "Grape", "Strawberry", "Watermelon")
var firstDiffer = ""
for (bob in bobLoveFruits) {
if (bob !in jayLoveFruits) {
firstDiffer = bob
break
}
}
println("jay doesn't like $firstDiffer")
}
// Output
// jay doesn't like Orange
bob 和 jay 都有自己喜欢的水果,我们的目标是,找到第一个 bob 喜欢,而 jay 不喜欢的水果。
代码中,Orange 和 Pineapple 都不是 jay 喜欢的,但是我们要求只返回第一个即可,这点请读者记住。
现在,我们做一下优化,让 for 循环内部的函数更加具有扩展性:
fun main() {
val bobLoveFruits = arrayOf("Apple", "Banana", "Orange", "Pineapple", "Watermelon")
val jayLoveFruits = arrayOf("Apple", "Banana", "Grape", "Strawberry", "Watermelon")
var firstDiffer = ""
for (bob in bobLoveFruits) {
validator(
bob = bob,
jayLoveFruits = jayLoveFruits,
check = { bob, jayLoveFruits -> bob !in jayLoveFruits },
onFailed = { bob ->
firstDiffer = bob
return // 这里为什么不用 break ?
}
)
}
println("jay doesn't like $firstDiffer")
}
private inline fun validator(
bob: String,
jayLoveFruits: Array<String>,
check: (String, Array<String>) -> Boolean,
onFailed: (String) -> Unit
) {
if (check(bob, jayLoveFruits)) {
onFailed(bob)
}
}
这段代码将 for 循环内部的逻辑提出来了一个 validator 的 inline 函数,同时将判断逻辑放到了两个 lambda 表达式中,我们期望效果和上述代码是一样的。
可能读者已经注意到了,我们在 onFailed 里面,用了 return,所以结果是,没有任何打印。
此处的 return,启用了 Non-local Return。即会返回整个函数,也就是 return main 函数,这就导致了后续的打印没有打印出来。
当然,return 可以添加一个标签来修改返回的作用域。
// ...
return@validator
// ...
// Output
// jay doesn't like Pineapple
我们期望返回的是 validator 函数,而不是整个 main 函数。但是结果却是错误的 Pineapple(正确答案应该是 Orange)。
因为 return@validator 并没有跳出循环。
现在,我们改成 break,期望它能跳出循环
// ...
onFailed = { bob ->
firstDiffer = bob
break
}
// ...
// Compile error:
// The feature "break continue in inline lambdas" is only available since language version 2.2
好的,我们获得了一个编译错误,当前笔者使用的 Kotlin 版本是 2.1.21,跨域跳转 还是 preview 阶段,所以,我们现在需要启用这个新特性。
启用新特性
首先,保证你的编译器启用了 K2 模式:
然后,在 gradle 中,配置新特性:
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xnon-local-break-continue")
}
}
此时,上述代码就可以通过编译了,运行上述代码:
// Outout
// jay doesn't like Orange
我们获得了正确的结果。
更进一步
如果读者查看 break,会发现编译器给予了一个提示:
Ambiguous non-local 'break' ('for' vs 'validator'). Use clarifying labels or add 'callsInPlace' contract.
为什么 for 和 validator 会让编译器觉得有歧义的?
实际上,break 和 continue 关键字只适用于真正的循环(for、while、do-while),从不适用于任何函数例如 forEach、filter、map。
编译器当然知道这个地方 break 是作用于谁,但是作为写代码的人,可能会产生歧义,所以编译器给予了这个提示。
有两个解决方案,我们先来看第一个 clarifying labels:
loop@ for (bob in bobLoveFruits) {
validator(
bob = bob,
jayLoveFruits = jayLoveFruits,
check = { bob, jayLoveFruits -> bob !in jayLoveFruits },
onFailed = { bob ->
firstDiffer = bob
break@loop
}
)
}
这样即可,添加一个标签。
第二种解决方案,add 'callsInPlace' contract。我们需要修改一下 validator 函数:
@OptIn(ExperimentalContracts::class) // 声明 OptIn
private inline fun validator(
bob: String,
jayLoveFruits: Array<String>,
check: (String, Array<String>) -> Boolean,
onFailed: (String) -> Unit
) {
// 声明 contract
contract {
callsInPlace(check, kind = InvocationKind.EXACTLY_ONCE) // check 只会调用一次
callsInPlace(onFailed, kind = InvocationKind.AT_MOST_ONCE) // onFailed 最多调用一次
}
if (check(bob, jayLoveFruits)) {
onFailed(bob)
}
}
如此这般,当我们再使用 break 时候,便不会有编译器提示了,因为我们已经告诉编译器(实际上是告诉自己),onFailed 不会调用多次。
语言特性的一大步
跨域跳转 特性的核心意义在于 提升代码的简洁性和控制精度,尤其在复杂的嵌套逻辑中能显著减少冗余代码,同时与 return 保持了语法的一致性,提升了语言的一致性,
在支持 跨域跳转 之后,Kotlin 就可以这样玩了:
与 Kotlin 原生 Result 一起共舞:
val bobLoveFruits = arrayOf("Apple", "Banana", "Carrot", "Pineapple")
for (bob in bobLoveFruits) {
requestFruitDetail(bob).fold(
onSuccess = {
println(it)
},
onFailure = {
println("$bob failure")
continue
}
)
}
fun requestFruitDetail(fruit: String): kotlin.Result<String> { }
与标签一起,想去哪儿就去哪儿:
outerLoop@ for (i in 1..3) {
innerLoop@ for (j in 1..3) {
if (i == 2 && j == 2) break@outerLoop // 直接跳出外层循环
println("i=$i, j=$j")
}
}
综上,用最少的代码表达最直接的意图,正是 Kotlin 追求简洁性与表达力典型设计。