有人认为 null
是 Java
最糟糕的设计之一,这个极其有面向对象特色的特殊值实际生产中带来了极大的不便,因为它使得 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
的位置非常清晰
链式使用
Option
的 orElse
方法接收另一个 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
,这和 List
中 flatMap
道理完全相同,两个嵌套的 Option
会被“拍扁”成为一个,由于 Option
本身只能含一个元素,多次嵌套也只能是一个,当然过程中只要 user
和 gender
一方为 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
模式也可以排除掉集合中 gender
为 None
的元素
for {
User(_, _, _, _, Some(gender)) <- UserRepository.findAll
} yield gender