1. 引言

与 Java 相比,Kotlin 在类型系统中引入了一个重要变化:可空性。在 Java 中我们需要显式使用 Optional 来表达可能为空的值,而在 Kotlin 中,所有值要么明确不能为 null,要么在访问时必须进行空值检查。

在 Java 中,null 值常常导致代码风格变得笨拙,空值检查会掩盖业务逻辑。本文将介绍一些使用 Kotlin 表达式处理空值的惯用方式,帮助我们写出更清晰、更具表现力的代码。

2. 什么是 Kotlin 的惯用写法

要讨论如何惯用地处理可空值,首先要明确什么是 Kotlin 的惯用风格。Kotlin 虽然与 Java 高度兼容,但其设计哲学更偏向函数式编程。

在类成员层面,Kotlin 推崇函数式范式:使用不可变结构、偏好纯函数,甚至将函数作为一等公民。此外,在函数式编程中,程序本质上就是一系列数据流的转换或过滤

因此,Kotlin、Scala 等语言非常推崇链式调用风格:

fun fluent(a: A): String = a
  .toB()
  .toC()
  .toString()

每一步变换都清晰明了,读者关注的是操作本身,而不是中间的数据结构。

另一个核心思想是让程序能够处理所有合法的输入。面向对象中这个概念体现为多态,而在函数式中则体现为模式匹配(pattern-matching):

fun patterMatching(a: Any): C = when(a) {
    is A -> TODO("处理 A 类型")
    is B -> TODO("处理 B 类型")
    is C -> TODO("继续处理")
    else -> TODO("默认处理")
}

当然,函数式编程的内涵远不止这两点,但这些已经足够帮助我们理解如何处理可空值了。

3. Elvis 运算符与返回值

首先,处理可空值时几乎都会用到 安全调用运算符(safe call operator)

nested?.value

我们可以链式使用这些安全调用:

nested?.value?.subvalue?.subsubvalue

它们的行为类似于 Maybe 单子:只要其中一步为 null,整个表达式的结果就是 null 否则返回最终的 subsubvalue

安全调用的好搭档是 Elvis 运算符(?:)

fun elvisStacking(flag: String?) =
  flag
    ?.let { transform(it) }
    ?.let { transformAgain(it) }
    ?: "erised"

本质上,它对可空类型进行折叠(fold),让我们可以为“不为 null 的情况”提供一个“否则”的分支。链式转换中可以包含多个步骤,最终用 Elvis 提供默认值,以应对链中某一步可能为 null 的情况。

通过安全调用和 Elvis 运算符,我们可以构建一个数据转换流水线,既能正确处理非空输入,也能在输入或某步转换结果为 null 时返回合适的默认值。

4. Elvis 运算符与返回 Unit

前面的例子适用于我们期望返回某个值的情况。但有时候我们希望函数只是执行副作用(side-effect),而不返回任何有意义的值。此时,Kotlin 提供了 Unit 类型来表示“无返回值”:

fun elvisStackingWithUnitDefault(flag: String?): Unit =
  flag
    ?.let { transform(it); Unit }
    ?: println("erised")

⚠️ 但要注意,依赖副作用的写法是一种危险的架构设计。通常来说,函数应尽量返回值,这样更容易测试和维护。

虽然上面的写法也使用了链式风格,但它违背了函数式编程的核心理念,因为我们在链中执行的是副作用,而不是数据转换。

建议:优先使用返回值的方式处理可空逻辑,避免滥用副作用。

5. if 表达式

虽然 Java 有三元运算符,但在很多情况下使用 if...else 更清晰。Kotlin 支持 if 表达式,它有返回值,适用于表达式式编程:

fun ifExpression(flag: String?) =
  if (flag.isNullOrBlank()) "erised" else transform(flag)

与 Java 不同的是,Kotlin 的 if 表达式必须有 else 分支才能作为表达式返回。

这种写法在我们不需要对值进行转换,而只是根据是否为 null 返回不同结果时,比安全调用更直观

例如,判断字符串是否为空并返回默认值时,使用 if 表达式更清晰易懂。

6. when 表达式

最后,Kotlin 的 when 是一个强大的模式匹配工具。它不仅能匹配值,还能匹配类型,甚至可以匹配对象实例:

data class Snitch(val content: Any)

fun whenExpression(flag: Any?) =
  when (flag) {
      null -> "erised"
      is Exception -> "expelliarmus"
      "socks" -> "Silente"
      "doe" -> "Piton"
      "family" -> "the boy"
      Snitch("the stone") -> "a hallow"
      else -> "babbano"
  }

在众多可能的值中,null 也可以作为一个匹配项。这种能力使 when 成为处理多个逻辑分支的理想选择

建议:当需要根据多个可能的值或类型做出不同处理时,优先使用 when

7. 总结

Kotlin 提供了多种处理可空值的方式,我们可以根据实际需求选择最合适的工具:

场景 推荐方式
单层空值处理 + 默认值 Elvis 运算符 ?:
多层转换 + 默认值 安全调用链 + Elvis
有副作用的操作 谨慎使用,优先返回值
条件判断 + 返回不同值 if 表达式
多种值/类型分支处理 when 表达式

根据实际业务逻辑选择合适的表达方式,能让我们的代码更简洁、更易读、更易维护。

所有示例代码都可以在 GitHub 上找到。


原始标题:Idiomatic Way to Treat Nullable Values