1. 概述

在本教程中,我们将学习如何在 Scala 中管理可选数据。Scala 的 Option 类型能帮助我们编写更健壮的代码,只要我们在整个应用代码中合理使用它。

2. Option 基础知识

处理可选值需要一些深思熟虑。我们都经历过:处理缺失数据的代码不仅难写,而且是很多运行时错误的根源。

Scala 的 Option 特别有用,因为它通过两种相互强化的方式来管理可选值:

  1. 类型安全 —— 我们可以参数化可选值。
  2. 函数式友好 —— Option 类型提供了一组强大的函数式操作,有助于减少 bug。

此外,学习和掌握 Option 类是接受 Scala 函数式编程范式的好方法。我们会逐步讲解。

2.1. Scala 中的 Option 表示

Scala 中的 Option 类型结构如下:

           Option[T]
              ^
              |
      +-------+------+
      |              |
      |              |
    Some[T]        None

基类 scala.Option 是抽象的,继承自 scala.collection.IterableOnce。这使得 Option 可以像容器一样使用。在后续讨论 mapfilter 时会体现这一点。

重要的是:Option 及其子类都需要一个具体类型,可以是显式声明的,也可以是类型推断的:

val o1: Option[Int] = None
val o2 = Some(10)

这两个变量都是 Option[Int] 类型。

✅ 此外,Option 是“null 感知”的:

val o1: Option[Int] = Option(null)
assert(false == o1.isDefined)

这在与 Java 交互时特别有用,尤其是处理那些用 null 表示“未找到”的旧 Java 库时。

2.2. 判断 OptionSome 还是 None

我们可以使用以下方法判断:

  • isDefined – 如果是 Some 返回 true
  • nonEmpty – 同上
  • isEmpty – 如果是 None 返回 true

2.3. 获取 Option 的值

可以通过 get 方法获取值。⚠️ 但如果对 None 调用 get,会抛出 NoSuchElementException。这种行为被称为“成功偏向”。

正因为如此,很多人会写出这样的代码:

val o1: Option[Int] = ...
val v1 = if (o1.isDefined) {
  o1.get
} else {
  0
}

或者使用模式匹配:

val o1: Option[Int] = ...
val v1 = o1 match {
  case Some(n) => n
  case None => 0
}

❌ 这些都是 Scala 社区中的反模式,因为它们依赖于非函数式结构。其他反模式还包括使用 if/elsematch 来映射 Option其实有更好的方式,我们后面会讲。

2.4. Option 的默认值方法

我们还有两个更安全的方法:

  • getOrElse – 如果是 Some,返回值;否则返回默认值
  • orElse – 如果是 Some,返回自身;否则返回另一个 Option

例如:

val v1 = o1.getOrElse(0)

更复杂的例子:

val usdVsZarFxRate: Option[BigDecimal] = ... 
val marketRate = usdVsZarFxRate.getOrElse(throw new RuntimeException("No exchange rate defined for USD/ZAR"))

3. 把 Option 当作容器

如前所述,Option 是另一个值的容器。可以把它看作只有一个元素的集合。因此,我们可以像遍历 List 一样遍历 Option,结合 Scala 的函数式特性,写出更简洁、更少出错的代码。

3.1. 映射 Option

Option.map 的定义如下:

final def map[B](f: (A) => B): Option[B]

示例:

val o1: Option[Int] = Some(10)
assert(o1.map(_.toString).contains("10"))
assert(o1.map(_ * 2.0).contains(20))

val o2: Option[Int] = None
assert(o2.map(_.toString).isEmpty)

map 可以转换类型。

此外,flatMap 可用于“压平”嵌套的 Option

举个例子,假设我们有以下结构:

trait Player {
  def name: String
  def getFavoriteTeam: Option[String]
}

trait Tournament {
  def getTopScore(team: String): Option[Int]
}

val player: Player = ...
val tournament: Tournament = ...

要获取玩家喜爱队伍的最高分,我们可以这样写:

def getTopScore(player: Player, tournament: Tournament): Option[(Player, Int)] = {
  player.getFavoriteTeam.flatMap(tournament.getTopScore).map(score => (player, score))
}

如果只想访问值,可以使用 foreach

val o1: Option[Int] = Some(10)
o1.foreach(println)  // 输出 10

3.2. 使用 Option 控制流程

我们也可以用 map 来控制流程:

val o1: Option[Int] = Some(10)
val o2: Option[Int] = None

def times2(n: Int): Int = n * 2

assert(o1.map(times2).contains(20))
assert(o2.map(times2).isEmpty)

⚠️ 在 o2.map(times2) 中,times2 并不会被调用。

3.3. 使用过滤器控制流程

Option 也支持类似集合的过滤方法:

  • filter – 如果是 Some 且满足条件,返回自身
  • filterNot – 如果是 Some 且不满足条件,返回自身
  • exists – 如果是 Some 且满足条件,返回 true
  • forall – 与 exists 相同(因为 Option 最多只有一个值)

示例:

def whoHasTopScoringTeam(playerA: Player, playerB: Player, tournament: Tournament): Option[(Player, Int)] = {
  getTopScore(playerA, tournament).foldRight(getTopScore(playerB, tournament)) {
    case (playerAInfo, playerBInfo) => playerBInfo.filter {
      case (_, scoreB) => scoreB > playerAInfo._2
    }.orElse(Some(playerAInfo))
  }
}

4. whenunless 构造器

Scala 2.13 引入了 whenunless 方法,用于根据条件构造 Option 对象。

4.1. when 构造器

定义如下:

def when[A](cond: Boolean)(a: => A): Option[A] =
  if (cond) Some(a) else None

示例:

val num: Int = 25
val maybePositive = Option.when(num > 0)(num)
assert(maybePositive == Some(25))

4.2. unless 构造器

when 相反:

def unless[A](cond: Boolean)(a: => A): Option[A] = 
  when(!cond)(a)

示例:

val num: Int = 25
val maybeNegative = Option.unless(num > 0)(num)
assert(maybeNegative == None)

4.3. 自定义 Option 构造器

我们可以使用隐式类来扩展类型,例如:

object OptionBuilder {
  implicit class OptionWhenBuilder[A](value: A) {
    def when(condition: A => Boolean): Option[A] = {
      Some(value).filter(condition)
    }
  }

  implicit class OptionUnlessBuilder[A](value: A) {
    def unless(condition: A => Boolean): Option[A] = {
      Some(value).filterNot(condition)
    }
  }
}

使用示例:

import OptionBuilder._

val num: Int = 25
val maybePositive = num.when(_ > 0)  // Some(25)
val maybeNegative = num.unless(_ > 0) // None

5. 总结

本文介绍了 Scala 的 Option 类型的基本用法,并展示了如何使用其函数式接口编写简洁、地道的 Scala 代码。我们还学习了 whenunless 构造器,用于根据条件创建 Option

完整代码可在 GitHub 获取。


原始标题:The Option Type in Scala