译者注:原文出处http://danielwestheide.com/blog/2012/12/19/the-neophytes-guide-to-scala-part-5-the-option-type.html,翻译:Thomas
在前面几篇章节中,我们高歌猛进的讨论了许多高级特性,尤其是关于模式匹配和提取器。现在是时候放慢脚步,来仔细看看Scala里更基础的一个特点:Option类型。
如果你已经参加过了Coursera里的Scala课程(译者注:还没有参加吗?赶快报名啊),你已经对这个类型有所了解,并且在Map的API里看到过了。在本系列的前篇中,当实现自己的提取器时我们也用到过Option。
当然,关于Option还有非常多的值得一说的方面。你也许会想知道这Option为啥值得花一个篇章来讲解,它在处理不存在数据的方式为什么比其它方式要好得多。你可能正在为如何在自己的代码中恰当的使用Option而犯迷糊。本篇的目标就是要解决这些所有的疑问,让如饥似渴的你学会所有关于Option你不得不知的事。
基本思想
如果你有Java的实战经验,你一定已经被NullPointerException折腾得够呛吧(其他语言会抛出类似的错误)。你会经常碰到一些方法在默写场景下返回个null,而你根本就没意识到它会给你个null,当然也不会去处理这种情况。为了表示一个缺失的值,null已经被滥用了。
有些语言以一些不一样的方式来处理null或者允许开发者稍微安全的来处理可能的null,如Grovvy就提供一个null安全的属性访问操作符,类似foo?.bar?.baz这样的写法,即使foo或bar为null时,它也不不会抛出异常。不过因为Grovvy并不会强迫你使用这个操作符,当你忘记用安全操作符时,上帝保佑吧。
Clojure基本上把nil值当成一个空字串、空列表、空map等来处理,这意味着nil会传递到调用上层。大多数情况下这样处理是可以的,但有时候这只是将异常带到更高一级而已,可能会有某一级没能很好的处理nil,结果就会变得糟糕了。
Scala试图通过完全消除null而用一个类型表示可选值来解决这些问题。就有了Option[A]这个trait。
Option[A]是一个类型为A的可选值的容器。如果类型A的值存在,Option[A]是一个Some[A]实例,里面保存着A类型的值。如果值不存在,Option[A]则会使None对象。
在类型层面标示值可能存在或不存在,编译器会强迫使用你代码的人来处理这种可能性。不存在说你期待一个值总是存在但实际上却可能不存在。
Option是强制的!不要使用null来表示一个可选值的不存在。
生成一个Option
通常,你可以简单的通过Some case class来构建一个Option[A]:
val greeting: Option[String] = Some("Hello world")
或者当值确实不存在时,只需赋值None即可:
val greeting: Option[String]=None
当然,有时候你可能还是需要和Java代码或其它的JVM语言打交道,这些语言可能还停留在使用null的远古时代。因此,Option联合对象提供一个统一的工厂方法来创建None或Some:
val absentGreeting: Option[String] = Option(null) // absentGreeting will be None
val presentGreeting: Option[String] = Option("Hello!") // presentGreeting will be Some("Hello!")
使用可选值
Option看上去挺精巧的,如果在你的代码中怎么来用他们呢?我们来举个例子。
想象一下你在为一家初创公司干活,首先你要帮助实现一个用户管理模块。需要通过id来查到用户。有时候查询请求提供的id是不存在在的。这个查询的函数返回一个Option[User]类型给调用者。它的实现模型可能像这样的:
case class User(
id: Int,
firstName: String,
lastName: String,
age: Int,
gender: Option[String])
object UserRepository {
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 = users.values
}
作为调用者,你收到了从UserREpository返回的Option[User]后,你该做些什么呢?
一种方式是通过Option的isDefined方法检查返回值是否包含值,如果返回true,则通过get方法获取值:
val user1=UserRepository.findById(1)
if(user1.isDefined)
println(user1.get.firstName)
// will print "John"
这看上去和Java的Guava类库提供的Optional类型很相似。如果你嫌这样的用法还是不方便,心想着Scala应该提供更大气的方式,那就对了。 上面的例子还有个严重问题,当你忘记了isDefined检查时,就可能会带来运行时错误,这也没比直接用null好到哪里去。
奉劝各位看官不要走这条邪路!
提供默认值
很多情形下,你需要在可选值缺失时提供fallback方案或者一个默认值。这个以通过Option的getOrElse方法来实现:
val user = User(2, "Johanna", "Doe", 30, None)
println("Gender: " + user.gender.getOrElse("not specified")) // will print "not specified"
你在getOrElse里提供的默认值是一个by-name形式的参数,也就是说只有当可选值缺失时,这个参数才会被计算。因此,你不必单行提供默认值会带来额外计算 -- 默认值只有在需要时才被计算。
模式匹配
Some是一个
case class, 所以完全可以在模式里用它,可以用在普通的模式匹配表达式或其它可以使用模式匹配的场景。用模式匹配来重写上上面的例子:
val user = User(2, "Johanna", "Doe", 30, None)
user.gender match {
case Some(gender) => println("Gender: " + gender)
case None => println("Gender: not specified")
}
或者你想要使用纯正的模式匹配表达式:
val user = User(2, "Johanna", "Doe", 30, None)
val result:String=user.gender match {
case Some(gender) => gender
case None => "Gender: not specified"
}
println("Gender: "+ result)
但愿你已经注意到,Option的模式匹配用法还是显得有点不够简练,这也是为啥我们习惯上也不用这种用法。不过既然大家对模式匹配都充满期待,我们来看看还有没有更好的方式。
下面你将会学到一个非常优雅的使用Option的方式。
Option可以被看做是集合
(译者注:从现在开始直到后面几篇的内容,如果你了解Monad的概念,会对你非常简单,看官可以快速浏览即可,还不懂Monad为何物的,推荐你看这篇博文:http://hongjiang.info/understand-monad-0/)
目前为止你是没看到太多优雅的或习惯的使用Option的场景,现在就来看看。
我已经提到过Option[A]是类型A的容器。你可能会把它想成是一个集合 - 一种有0或一个A类型元素的特殊集合。这是个非常强大的概念!
虽然从类型层面来看,Option并不是Scala的集合类型,你仍然可以像从Scala的集合类型,如List,Set,中获得的所有好处一样来用Option。如果真的需要,你甚至可以把一个Option转成List。
那么你具体可以怎么做呢?
可选值存在时执行副作用
当给定的可选值存在时,如果你只想要执行些副作用,Scala的集合的foreach方法在Option里也存在:
UserRepository.findById(2).foreach(user => println(user.firstName)) // prints "Johanna"
传递给foreach的函数会被呼叫一次或不被呼叫,这依赖于Option是个Some还是None。
Map一个Option
用集合的思想来用Option还带来一些很棒的结果,你可以用函数式的用法来用Option,就如同你操作List、Set时一样。
就像你可以用map操作把List[A]
转化成 List[B]
, 你也可以用map操作把Option[A]转化成
Option[B]
. 也就是说如果你的Option[A]是一个Some[A],那么map的结果会是Some[B],否则map结果会是None。
你可以把None当做是空List来理解:当你对一个空的List[A]做map操作时,你会得到一个空的List[B];当你对一个为None的Option[A]做map操作时,你会得到一个Option[B]类型的None。
我们来获取一个可能存在的用户的年龄:
val age = UserRepository.findById(1).map(_.age) // age is Some(32)
flatMap和Option
我们来获取一个用户的性别:
val gender = UserRepository.findById(1).map(_.gender) // gender is an Option[Option[String]]
gender的类型是Option[Option[String]]
. 为什么呢?
这样来看:你原来有个包着User的Option,后来你将User map到Option[String](这是gender属性的类型).
这种嵌套的option看着挺乱的吧,所以和所有集合一样,Option也提供了一个flatMap方法。就像你可以将List[List[A]] flatMap到List[B]一样,也同样适用Option[Option[A]]
:
val gender1 = UserRepository.findById(1).flatMap(_.gender) // gender is Some("male")
val gender2 = UserRepository.findById(2).flatMap(_.gender) // gender is None
val gender3 = UserRepository.findById(3).flatMap(_.gender) // gender is None
现在返回的类型是Option[String]了。如果user有定义并且他得gender属性也有值,我们会得到一个单层的Some。如果user无值或者他的gender没有值,我们会最终得到None。
为了理解工作原理,我们来看下当flatMap一个字串的List时发生了些什么,始终应该在脑子里记住一个Option就像一个List一样,也是集合:
val names: List[List[String]] =
List(List("John", "Johanna", "Daniel"), List(), List("Doe", "Westheide"))
names.map(_.map(_.toUpperCase))
// results in List(List("JOHN", "JOHANNA", "DANIEL"), List(), List("DOE", "WESTHEIDE"))
names.flatMap(_.map(_.toUpperCase))
// results in List("JOHN", "JOHANNA", "DANIEL", "DOE", "WESTHEIDE")
用flatMap时,内嵌的list中的元素被转换成一个单层字串列表。显而易见,内嵌的空列表不会留下什么。
再回到Option类型,考虑一下你map一个包在Option里的字串的列表的情形:
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")
如果你还是用map来套用Option的list,结果会是List[Option[String]]。如果改用
flatMap,所有内嵌集合中的元素会被放入一个单层列表:
列表中所有Some[String]元素被解包并置入结果列表中,因为None不包含任何值,所以没有什么数值可解包的,就会被跳过。基于这样的解释,请再回头看一下flatMap是如何用在Option上的。
过滤option
你可以像过滤list一样过滤Option。如果Option[A]是一个Some[A]并且过滤器返回true,那么Some[A]本身被返回。如果Option实例是None或者过滤器返回false,则返回None:
UserRepository.findById(1).filter(_.age > 30) // None, because age is <= 30
UserRepository.findById(2).filter(_.age > 30) // Some(user), because age is > 30
UserRepository.findById(3).filter(_.age > 30) // None, because user is already None
For语法
你已经知道了Option可以被看做是一种集合,它提供了类似集合的map
, flatMap
,filter
这些方法,你可能已经在猜想是否可以用for语法来处理Option。通常,这是在和option打交道时可读性最好的一种用法,尤其是当你需要串联许多的 map
, flatMap
和 filter时。当然如果只有一个map时,直接用map还是更简短一些。
我国我们想要获取一个用户的性别,可以用下面的for语句:
for {
user <- UserRepository.findById(1)
gender <- user.gender
} yield gender // results in Some("male")
如同你对list的这种用法所了解的, 上面的代码实现和flatMap一样的功能。如果findById返回了None或Gender是None,for语句的返回就是None。在上面的例子中,应为gender有定义,所以返回的是Some。
如果我们想要获取所有用户的性别,我们需要遍历所有user,为每个user生成性别:
for{
user <-UserRepository.findAll
gender <-user.gender
}yield gender
因为上述过程已经进行了高效的flatMap,for返回的结果会是List[String]。因为只有一个用户定义了性别,
返回的值为List("male")。
用在generator的左侧
你或许还记得在第三篇中讲的,for语句中generator左侧是一个模式。这意味着你可以在for语句中模式化涉及到的option。我么来将上面的例子改写一下:
for{
User(_,_,_,_,Some(gender)) <-UserRepository.findAll
}yield gender
在generator的左侧使用一个Some模式会自动的将None的元素剔除掉。
Option的串联
Option也可以被串联起来,这有点类似偏函数的串联。通过呼叫一个Option实例的orElse方法来实现,传递另外一个Option实例作为by-name参数给orElse。如果第一个实例为None,orElse返回第二个实例,否则返回第一个实例。一个用得上的场景是查找资源,如果你有多个不同优先级的资源来源地方用来找寻资源,像下面的例子一样,在config目录下的资源应该被优先使用,所以我们呼叫这个资源地址的orElse,并传递给它一个候选Option:
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
这通常用于你有多个Option可选的场景,如果你只是想为一个Option提供一个默认值,getOrElse或许更合适。
总结
但愿我在本篇把和Option有关的所有知识点都给你了,这样你会从熟练使用Option中受益匪浅,也能理解他人写的Scala代码,你自己也可以写出可读性更好,更加函数化的代码。从本篇中你还应该已经领会了一个重要的概念,那就是list,map,set,Option和其它一些类型的共同点,使用它们的一些共性,这些共性是非常优雅和强大的。
在接下来一个篇章里,我会开讲在Scala里如何以习惯的、函数化的方式来处理错误。
作者:Daniel Westheide,2012.12.19