1. 概述
在本教程中,我们将学习如何在 Scala 中管理可选数据。Scala 的 Option
类型能帮助我们编写更健壮的代码,只要我们在整个应用代码中合理使用它。
2. Option
基础知识
处理可选值需要一些深思熟虑。我们都经历过:处理缺失数据的代码不仅难写,而且是很多运行时错误的根源。
Scala 的 Option
特别有用,因为它通过两种相互强化的方式来管理可选值:
- ✅ 类型安全 —— 我们可以参数化可选值。
- ✅ 函数式友好 ——
Option
类型提供了一组强大的函数式操作,有助于减少 bug。
此外,学习和掌握 Option
类是接受 Scala 函数式编程范式的好方法。我们会逐步讲解。
2.1. Scala 中的 Option
表示
Scala 中的 Option
类型结构如下:
Option[T]
^
|
+-------+------+
| |
| |
Some[T] None
基类 scala.Option
是抽象的,继承自 scala.collection.IterableOnce
。这使得 Option
可以像容器一样使用。在后续讨论 map
和 filter
时会体现这一点。
重要的是: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. 判断 Option
是 Some
还是 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/else
或 match
来映射 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. when
和 unless
构造器
✅ Scala 2.13 引入了 when
和 unless
方法,用于根据条件构造 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 代码。我们还学习了 when
和 unless
构造器,用于根据条件创建 Option
。
完整代码可在 GitHub 获取。