在 Kotlin `when` 语句(或其他分支构造)中包含函数或 lambda 作为条件的最简洁方法是什么?

What is the most concise way to include functions or lambdas as conditions in a Kotlin `when` statement (or other branching construct)?

我正在处理字符串,我遇到了 answer: that one can put regular expressions in a when statement with a custom class that overrides equals. While this does effectively use the type system to shoehorn syntactic sugar into the when statement, I find the following pretty ugly, and would never do this in code that I intend to share with another developer (quoting travis):

import kotlin.text.regex

when (RegexWhenArgument(uri)) {
    Regex(/* pattern */) -> /* do stuff */
    Regex(/* pattern */) -> /* do stuff */
    /* etc */
}

其中 RegexWhenArgument 最小定义为:

class RegexWhenArgument (val whenArgument: CharSequence) {
    operator fun equals(whenEntry: Regex) = whenEntry.matches(whenArgument)
    override operator fun equals(whenEntry: Any?) = (whenArgument == whenEntry)
}

(引用结束)

我认为将 arg 传递给 when 然后引用对 arg 类型进行操作的函数会更具可读性。举个例子:

// local declaration
val startsWithFn: (String) -> Boolean = {s -> s.startsWith("fn:")}

when(givenString) {
    ::startsWithHelp -> printHelp()
    startsWithFn -> println("Hello, ${givenString.substring(3)}!")
}

// package level function
fun startsWithHelp(s:String) = s.startsWith("help", true)

当然,这段代码无法编译。有没有一种可读、可维护且简洁的方法来做到这一点?也许使用流?经验丰富的 Kotlin 开发人员会做什么?

您的问题有很多解决方案。我将从简单的开始,然后再转向更复杂的。


没有参数时

Kotlin documentation 说:

when can also be used as a replacement for an if-else if chain. If no argument is supplied, the branch conditions are simply boolean expressions, and a branch is executed when its condition is true

用例:

when {
    startsWithHelp(givenString) -> printHelp()
    startsWithFn(givenString) -> println("Hello, ${givenString.substring(3)}!")
}

优点:

  1. 不需要任何额外的代码

  2. 通俗易懂

缺点

  1. 样板(givenString)

覆盖 equals 的参数包装器

所需的包装器类似于 RegexWhenArgument,但不是检查 Regex,而是调用 (String) -> BooleanString没什么特别的,所以我就用<T>.

包装函数:

fun <T> whenArg(arg: T) = object {
    override fun equals(other: Any?) =
        if (other is Function1<*, *>) 
            (other as Function1<T, Boolean>)(arg)
        else arg == other
}

用例:

when (whenArg(givenString)) {
    ::startsWithHelp -> printHelp()
    startsWithFn -> println("Hello, ${givenString.substring(3)}!")
}

优点:

  1. 函数可以在when个分支中使用
  2. 非常容易理解
  3. 只需要一个简单的包装函数

缺点:

  1. 很容易忘记包装参数
  2. 在分支条件中可能会意外使用错误类型的函数

注意:这个解决方案和下一个允许使用函数组合,可以通过这些扩展创建:

infix fun <T> ((T) -> Boolean).and(other: ((T) -> Boolean)) = { it: T -> invoke(it) && other(it) }
infix fun <T> ((T) -> Boolean).or(other: ((T) -> Boolean)) = { it: T -> invoke(it) || other(it) }
operator fun <T> ((T) -> Boolean).not() = { it: T -> !invoke(it) }

基于 DSL when 模拟

在 Kotlin 中,DSL 不仅可以用于构建对象,还可以用于创建自定义程序流结构。

源代码:

@DslMarker
annotation class WhichDsl

// This object is used for preventing client code from creating nested
// branches. You can omit it if you need them, but I highly recommend
// not to do this because nested branches may be confusing. 
@WhichDsl
object WhichCase

// R type parameter represents a type of expression result
@WhichDsl
class Which<T, R>(val arg: T) {
    // R? is not used here because R can be nullable itself 
    var result: Holder<R>? = null

    inline operator fun ((T) -> Boolean).invoke(code: WhichCase.() -> R) {
        if (result == null && invoke(arg)) result = Holder(code(WhichCase))
    }

    // else analog
    inline fun other(code: WhichCase.() -> R) = result?.element ?: code(WhichCase)
}

data class Holder<out T>(val element: T)

inline fun <T, R> which(arg: T, @BuilderInference code: Which<T, R>.() -> R) =
    Which<T, R>(arg).code()

语句用例:

which(givenString) {
    ::startsWithHelp { printHelp() }
    startsWithFn { println("Hello, ${givenString.substring(3)}!") }
}

表达式用例:

val int = which(givenString) {
    ::startsWithHelp { 0 }
    startsWithFn { 1 }
    other { error("Unknown command: $givenString") }
}

优点:

  1. 可自定义的语法
  2. 可以添加新功能
  3. 可以添加仅适用于某些参数类型的函数
  4. 可以启用嵌套分支
  5. 没有样板文件
  6. 编译时检查函数类型
  7. 易于阅读

缺点:

  1. 需要一些助手 类 和函数
  2. 实验性用法@BuilderInference(可以用表达式的显式类型声明和语句的单独方法代替)
  3. 新语法学习可能需要一些时间

扩展示例:

inline fun <R> Which<*, R>.orThrow(message: () -> String) =
    other { throw NoWhenBranchMatchedException(message()) }

val <R> Which<*, R>.orThrow get() = orThrow { "No branch matches to $arg" }

inline fun <T, R> Which<T, R>.branch(condition: (T) -> Boolean, code: WhichCase.() -> R) =
    condition(code)

inline fun <T, R> Which<T, R>.case(value: T, code: WhichCase.() -> R) =
    branch({ it == value }, code)

fun <T : CharSequence, R> Which<T, R>.regex(regex: Regex, code: WhichCase.() -> R) =
    branch({ regex.matches(it) }, code)

fun <T : Comparable<T>, R> Which<T, R>.range(range: ClosedRange<T>, code: WhichCase.() -> R) =
    branch({ it in range }, code)

inline fun <T> Which<T, *>.sideBranch(condition: (T) -> Boolean, code: WhichCase.() -> Unit) {
    if (condition(arg)) code(WhichCase)
}

fun <T> Which<T, *>.sideCase(value: T, code: WhichCase.() -> Unit) =
    sideBranch({ it == value }, code)

inline fun <R> Which<*, R>.dropResult(condition: WhichCase.(R) -> Boolean = { _ -> true }) {
    result?.let { (element) ->
        if (WhichCase.condition(element)) result = null
    }
}

inline fun <T, R> Which<T, R>.subWhich(condition: (T) -> Boolean, code: Which<T, R>.() -> R) =
    branch(condition) {
    which(this@subBranch.arg, code)
}