尝鲜 Kotlin 的 Non-Local-Break

978 阅读3分钟

1.jpg

Kotlin2.1.0 版本时,发布了一个 preview 级别的特性 Non-local break and continue

英文名有点长,这里笔者简短的翻译成——跨域跳转

该特性允许你使用非局部的 breakcontinue,扩展了你在内联函数范围内可以使用的工具集,并减少了项目中的样板代码。

这篇文章我们一起来探究下,跨域跳转 有何魔力。

开胃菜

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

bobjay 都有自己喜欢的水果,我们的目标是,找到第一个 bob 喜欢,而 jay 不喜欢的水果。

代码中,OrangePineapple 都不是 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 循环内部的逻辑提出来了一个 validatorinline 函数,同时将判断逻辑放到了两个 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 模式:

2.png

然后,在 gradle 中,配置新特性:

kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xnon-local-break-continue")
    }
}

此时,上述代码就可以通过编译了,运行上述代码:

// Outout
// jay doesn't like Orange

我们获得了正确的结果。

更进一步

3.jpg

如果读者查看 break,会发现编译器给予了一个提示:

image.png

Ambiguous non-local 'break' ('for' vs 'validator'). Use clarifying labels or add 'callsInPlace' contract.

为什么 forvalidator 会让编译器觉得有歧义的?

实际上,breakcontinue 关键字只适用于真正的循环(forwhiledo-while),从不适用于任何函数例如 forEachfiltermap

编译器当然知道这个地方 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 不会调用多次。

语言特性的一大步

5.jpg

跨域跳转 特性的核心意义在于 提升代码的简洁性和控制精度,尤其在复杂的嵌套逻辑中能显著减少冗余代码,同时与 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 追求简洁性与表达力典型设计。