返回
Featured image of post Scala - 类型 Option

Scala - 类型 Option

Scala 中的 Option 类似 Java Optional 类,但使用更加方便,可直接用于模式识别,且可以像集合一样使用

有人认为 nullJava 最糟糕的设计之一,这个极其有面向对象特色的特殊值实际生产中带来了极大的不便,因为它使得 NullPointerException 成为最常见的异常

为了搞定这个问题,Groovy 有安全运算符 (Safe Navigation Operator) ?. 来防止在对象是 null 是引发异常,而是直接返回 null ,Clojure 则将 null 弱化为了 nil ,使它可以在调用层级中向上冒泡,将问题转嫁到高层

Java 8 提供了 Optional<T> 类来解决这个问题,Scala 中的 Option[T] 则更加方便,因为 Java 原生的 Optional<T> 无论如何自己不可能是 null ,而 Scala 中的 Option[T] 在值不存在时就是对象 None,因此 Option 的使用是强制的,可以完全替代 None 来表示一个值的缺失

示例代码场景:

case class User (
id: Int,
firstName: String,
lastName: String,
age: Int,
gender: Option[String]
)

object User {
private val users = Map(1 -> User(1, "John", "Doe", 32, Some("male")),
                       2 -> User(2, "Johanna", "Doe", 30, None))
def findById(id: Int): Option[User] = users.get(id)
def findAll: Iterable[User] = users.values
}

创建

如果值并不缺失,Option[A] 就是一个 Some[A] ,所以可以直接实例化为 Some

val existed: Option[String] = Some("Hello world")

而在缺失情况下直接使用 None 对象就行了

val absent: Option[String] = None

或者以 null 为入参,这样写代码与 Java 库更通用

val absent: Option[String] = Option(null)

使用默认值

虽然有 isDefined 方法来检查值是否存在,但这样做太过笨重,大部分时候还是使用 getOrElse 提供默认值更方便

我们对 gender 直接访问

val user = User(2, "Johanna", "Doe", 30, None)
println("Gender: " + user.gender.getOrElse("not specified")) // will print "not specified"

从这个样例类的构造也可以看出,Option 的使用使得可能出现 None 的位置非常清晰

链式使用

OptionorElse 方法接收另一个 Option 为参数,用于解决多个 Option 取第一个非 None 来进行使用的情况,也就是 getOrElse 的链式情况

典型的使用案例就是按优先级对多个不同的目录进行搜索,如下面例子,首先搜索 config 文件夹,然后调用 orElse 方法,以传递备用目录:

case class Resource(content: String)
val resourceFromConfigDir: Option[Resource] = None
val resourceFromClasspath: Option[Resource] = Some(Resource("I was found on the classpath"))
val resource = resourceFromConfigDir orElse resourceFromClasspath

模式匹配

由于 Some 是样例类,所以可以使用在模式匹配中:

user.gender.match {
  case Some(gender) => println("Gender: " + gender)
  case None => println("Gender: not specified")
}

在这里使用模式匹配并不方便,甚至非常多余,更常简的还是使用默认值的方法

同样,Option 也能使用 filter ,如下面这个例子,这就非常像一个集合了

User.findById(1).filter(_.age > 30) // None, because age is <= 30
User.findById(2).filter(_.age > 30) // Some(user), because age is > 30
User.findById(3).filter(_.age > 30) // None, because user is already None

Option 作为集合

我们知道 User.findById 得到的结果是一个 Option[User] ,这个 Option 对象完全可以当作 集合 (Collection) 使用,如使用 foreach, map, flatMap 进行操作

比方说 foreach,我们知道 Option 作为一个容器,包含元素个数不是 1 就是 0,所以 foreach 函数最多只会被调用一次,而如果是 None 则一次也不会被调用

User.findById(2).foreach(user => println(user.firstName))

Option 也可以用 map ,我们知道例子中 age 返回结果一定是一个 Int ,因此在 Option[User] 中访问年龄,得到的一定是一个 Option[Int] ,可能是 Some(Int) ,如果 Option[User] 不存在则结果还是 None

User.findById(1).map(_.age)

但如果我们访问 gender ,由于 gender 本身就是一个 Option 容器,直接用 Map 可能会造成嵌套,带来一个 Option[Option[String]] 作为结果

User.findById(1).map(_.gender)

这种情况下就可以使用 flatMap ,这和 ListflatMap 道理完全相同,两个嵌套的 Option 会被“拍扁”成为一个,由于 Option 本身只能含一个元素,多次嵌套也只能是一个,当然过程中只要 usergender 一方为 None ,结果就是 None

val gender1 = User.findById(1).flatMap(_.gender) // gender is Some("male")
val gender2 = User.findById(2).flatMap(_.gender) // gender is None
val gender3 = User.findById(3).flatMap(_.gender) // gender is None

而如果我们操作一个包含 Option 的列表,效果也是类似的,只是 None 会直接被过滤,不出现在结果中

val names: List[Option[String]] = List(Some("Johanna"), None, Some("Daniel"))
names.map(_.map(_.toUpperCase)) // List(Some("JOHANNA"), None, Some("DANIEL"))
names.flatMap(xs => xs.map(_.toUpperCase)) // List("JOHANNA", "DANIEL")

在 for 循环中使用

既然已经可以当作集合使用了,直接用 for 语句代替 flatMap 肯定也是可以的

for {
  user <- User.findById(1)
  gender <- user.gender
} yield gender // results in Some("male")

结合 for 循环与模式匹配使用

生成器左侧也是一个模式,通过在生成器左侧用 Some 模式也可以排除掉集合中 genderNone 的元素

for {
  User(_, _, _, _, Some(gender)) <- UserRepository.findAll
} yield gender
comments powered by Disqus