Scala新手指南中文版 -第五篇 The Option Type(Option类型)

Scala新手指南中文版 -第五篇 The Option Type(Option类型)

    博客分类: 
  • Scala
Scala Functional Programming 

译者注:原文出处http://danielwestheide.com/blog/2012/12/19/the-neophytes-guide-to-scala-part-5-the-option-type.html,翻译:Thomas

 http://thomassun.iteye.com/blog/2078490

在前面几篇章节中,我们高歌猛进的讨论了许多高级特性,尤其是关于模式匹配和提取器。现在是时候放慢脚步,来仔细看看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]:

 

Java代码   收藏代码
  1. val greeting: Option[String] = Some("Hello world")  
或者当值确实不存在时,只需赋值None即可:

 

 

Java代码   收藏代码
  1. val greeting: Option[String]=None  

 

当然,有时候你可能还是需要和Java代码或其它的JVM语言打交道,这些语言可能还停留在使用null的远古时代。因此,Option联合对象提供一个统一的工厂方法来创建None或Some:

 

Java代码   收藏代码
  1. val absentGreeting: Option[String] = Option(null// absentGreeting will be None  
  2. val presentGreeting: Option[String] = Option("Hello!"// presentGreeting will be Some("Hello!")  

 

使用可选值

Option看上去挺精巧的,如果在你的代码中怎么来用他们呢?我们来举个例子。

想象一下你在为一家初创公司干活,首先你要帮助实现一个用户管理模块。需要通过id来查到用户。有时候查询请求提供的id是不存在在的。这个查询的函数返回一个Option[User]类型给调用者。它的实现模型可能像这样的:

 

Java代码   收藏代码
  1. case class User(  
  2.   id: Int,  
  3.   firstName: String,  
  4.   lastName: String,  
  5.   age: Int,  
  6.   gender: Option[String])  
  7.   
  8. object UserRepository {  
  9.   private val users = Map(1 -> User(1"John""Doe"32, Some("male")),  
  10.                           2 -> User(2"Johanna""Doe"30, None))  
  11.   def findById(id: Int): Option[User] = users.get(id)  
  12.   def findAll = users.values  
  13. }  

 

作为调用者,你收到了从UserREpository返回的Option[User]后,你该做些什么呢?

一种方式是通过Option的isDefined方法检查返回值是否包含值,如果返回true,则通过get方法获取值:

 

Java代码   收藏代码
  1. val user1=UserRepository.findById(1)  
  2. if(user1.isDefined)  
  3.   println(user1.get.firstName)  
  4. // will print "John"  

 

这看上去和Java的Guava类库提供的Optional类型很相似。如果你嫌这样的用法还是不方便,心想着Scala应该提供更大气的方式,那就对了。 上面的例子还有个严重问题,当你忘记了isDefined检查时,就可能会带来运行时错误,这也没比直接用null好到哪里去。

奉劝各位看官不要走这条邪路!

 

提供默认值

很多情形下,你需要在可选值缺失时提供fallback方案或者一个默认值。这个以通过Option的getOrElse方法来实现:

 

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

 

你在getOrElse里提供的默认值是一个by-name形式的参数,也就是说只有当可选值缺失时,这个参数才会被计算。因此,你不必单行提供默认值会带来额外计算 -- 默认值只有在需要时才被计算。

模式匹配

 

 

Some是一个case class, 所以完全可以在模式里用它,可以用在普通的模式匹配表达式或其它可以使用模式匹配的场景。用模式匹配来重写上上面的例子:

 

Java代码   收藏代码
  1. val user = User(2"Johanna""Doe"30, None)  
  2. user.gender match {  
  3.   case Some(gender) => println("Gender: " + gender)  
  4.   case None => println("Gender: not specified")  
  5. }  
或者你想要使用纯正的模式匹配表达式:

 

 

Java代码   收藏代码
  1. val user = User(2"Johanna""Doe"30, None)  
  2. val result:String=user.gender match {  
  3.   case Some(gender) =>   gender  
  4.   case None => "Gender: not specified"  
  5. }  
  6. 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里也存在:

Java代码   收藏代码
  1. 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。

我们来获取一个可能存在的用户的年龄:

Java代码   收藏代码
  1. val age = UserRepository.findById(1).map(_.age) // age is Some(32)  

flatMap和Option

 

我们来获取一个用户的性别:

Java代码   收藏代码
  1. 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]]:

 
Java代码   收藏代码
  1. val gender1 = UserRepository.findById(1).flatMap(_.gender) // gender is Some("male")  
  2. val gender2 = UserRepository.findById(2).flatMap(_.gender) // gender is None  
  3. val gender3 = UserRepository.findById(3).flatMap(_.gender) // gender is None  

现在返回的类型是Option[String]了。如果user有定义并且他得gender属性也有值,我们会得到一个单层的Some。如果user无值或者他的gender没有值,我们会最终得到None。

为了理解工作原理,我们来看下当flatMap一个字串的List时发生了些什么,始终应该在脑子里记住一个Option就像一个List一样,也是集合:

Java代码   收藏代码
  1. val names: List[List[String]] =  
  2.   List(List("John""Johanna""Daniel"), List(), List("Doe""Westheide"))  
  3. names.map(_.map(_.toUpperCase))  
  4. // results in List(List("JOHN", "JOHANNA", "DANIEL"), List(), List("DOE", "WESTHEIDE"))  
  5. names.flatMap(_.map(_.toUpperCase))  
  6. // results in List("JOHN", "JOHANNA", "DANIEL", "DOE", "WESTHEIDE")  

用flatMap时,内嵌的list中的元素被转换成一个单层字串列表。显而易见,内嵌的空列表不会留下什么。

再回到Option类型,考虑一下你map一个包在Option里的字串的列表的情形:

Java代码   收藏代码
  1. val names: List[Option[String]] = List(Some("Johanna"), None, Some("Daniel"))  
  2. names.map(_.map(_.toUpperCase)) // List(Some("JOHANNA"), None, Some("DANIEL"))  
  3. 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:

Java代码   收藏代码
  1. UserRepository.findById(1).filter(_.age > 30// None, because age is <= 30  
  2. UserRepository.findById(2).filter(_.age > 30// Some(user), because age is > 30  
  3. UserRepository.findById(3).filter(_.age > 30// None, because user is already None  

 

For语法

你已经知道了Option可以被看做是一种集合,它提供了类似集合的mapflatMap,filter 这些方法,你可能已经在猜想是否可以用for语法来处理Option。通常,这是在和option打交道时可读性最好的一种用法,尤其是当你需要串联许多的 mapflatMap 和 filter时。当然如果只有一个map时,直接用map还是更简短一些。

我国我们想要获取一个用户的性别,可以用下面的for语句:

Java代码   收藏代码
  1. for {  
  2.   user <- UserRepository.findById(1)  
  3.   gender <- user.gender  
  4. } yield gender // results in Some("male")  

如同你对list的这种用法所了解的, 上面的代码实现和flatMap一样的功能。如果findById返回了None或Gender是None,for语句的返回就是None。在上面的例子中,应为gender有定义,所以返回的是Some。

如果我们想要获取所有用户的性别,我们需要遍历所有user,为每个user生成性别:

Java代码   收藏代码
  1. for{  
  2. user <-UserRepository.findAll  
  3. gender <-user.gender  
  4. }yield gender  

因为上述过程已经进行了高效的flatMap,for返回的结果会是List[String]。因为只有一个用户定义了性别,返回的值为List("male")。

用在generator的左侧

你或许还记得在第三篇中讲的,for语句中generator左侧是一个模式。这意味着你可以在for语句中模式化涉及到的option。我么来将上面的例子改写一下:

Java代码   收藏代码
  1. for{  
  2. User(_,_,_,_,Some(gender)) <-UserRepository.findAll  
  3.   
  4. }yield gender  

在generator的左侧使用一个Some模式会自动的将None的元素剔除掉。

Option的串联

Option也可以被串联起来,这有点类似偏函数的串联。通过呼叫一个Option实例的orElse方法来实现,传递另外一个Option实例作为by-name参数给orElse。如果第一个实例为None,orElse返回第二个实例,否则返回第一个实例。一个用得上的场景是查找资源,如果你有多个不同优先级的资源来源地方用来找寻资源,像下面的例子一样,在config目录下的资源应该被优先使用,所以我们呼叫这个资源地址的orElse,并传递给它一个候选Option:

Java代码   收藏代码
  1. case class Resource(content: String)  
  2. val resourceFromConfigDir: Option[Resource] = None  
  3. val resourceFromClasspath: Option[Resource] = Some(Resource("I was found on the classpath"))  
  4. val resource = resourceFromConfigDir orElse resourceFromClasspath  

这通常用于你有多个Option可选的场景,如果你只是想为一个Option提供一个默认值,getOrElse或许更合适。

总结

但愿我在本篇把和Option有关的所有知识点都给你了,这样你会从熟练使用Option中受益匪浅,也能理解他人写的Scala代码,你自己也可以写出可读性更好,更加函数化的代码。从本篇中你还应该已经领会了一个重要的概念,那就是list,map,set,Option和其它一些类型的共同点,使用它们的一些共性,这些共性是非常优雅和强大的。

在接下来一个篇章里,我会开讲在Scala里如何以习惯的、函数化的方式来处理错误。

作者:Daniel Westheide,2012.12.19

你可能感兴趣的:(Scala新手指南中文版 -第五篇 The Option Type(Option类型))