译者注:原文出处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
相关推荐
包含翻译后的API文档:scala-xml_2.12-1.0.6-javadoc-API文档-中文(简体)版.zip; Maven坐标:org.scala-lang.modules:scala-xml_2.12:1.0.6; 标签:scala、lang、modules、xml、中文文档、jar包、java; 使用方法...
包含翻译后的API文档:scala-compiler-2.12.7-javadoc-API文档-中文(简体)版.zip; Maven坐标:org.scala-lang:scala-compiler:2.12.7; 标签:scala、lang、compiler、中文文档、jar包、java; 使用方法:解压翻译...
包含翻译后的API文档:flink-scala_2.12-1.14.3-javadoc-API文档-中文(简体)版.zip 对应Maven信息:groupId:org.apache.flink,artifactId:flink-scala_2.12,version:1.14.3 使用方法:解压翻译后的API文档,用...
例如,它可以为Scala项目自动生成构建文件(如.sbt或.build.properties),帮助开发者编写类型安全的代码,并提供跳转到定义、查找引用和重构等功能。此外,它还支持ScalaTest和ScalaCheck等测试框架,便于进行单元...
标签:11、parser、scala、combinators_2、lang、modules、jar包、java、API文档、中文版; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档内容。 人性化翻译,文档中的代码和结构...
包含翻译后的API文档:scala-compiler-2.11.8-javadoc-API文档-中文(简体)-英语-对照版.zip; Maven坐标:org.scala-lang:scala-compiler:2.11.8; 标签:scala、lang、compiler、中英对照文档、jar包、java; 使用...
5. **额外的工具和库**:可能还包括Scala相关的工具和库,例如 sbt(Scala构建工具)的集成,或者用于Akka、Play Framework等Scala流行框架的支持。 使用这个插件,开发者可以享受到以下优势: - **语法感知**:...
包含翻译后的API文档:scala-reflect-2.11.8-javadoc-API文档-中文(简体)-英语-对照版.zip; Maven坐标:org.scala-lang:scala-reflect:2.11.8; 标签:reflect、scala、lang、jar包、java、API文档、中英对照版; ...
scala-SDK-4.7.0-vfinal-2.12-linux.gtk.x86_64.tar.gz scala-SDK-4.7.0-vfinal-2.12-linux.gtk.x86_64.tar.gz
scala-intellij-bin-2018.3.2.zip插件,亲测可用!!!scala-intellij-bin-2018.3.2.zip插件,亲测可用!!!scala-intellij-bin-2018.3.2.zip插件,亲测可用!!!
- **类型系统**:Scala具有强类型,支持类型推断,使得代码更简洁。 - **模式匹配**:允许开发者以一种声明式的方式处理数据结构。 - **高阶函数**:函数可以作为参数传递,也可以作为返回值。 - **类与对象**:...
Scala是一种强大的多范式编程语言,它融合了面向对象和函数式编程的特性。IntelliJ IDEA是一款广受赞誉的Java集成开发环境(IDE),它为开发者提供了丰富的工具和功能来提升开发效率。"Scala-intellij-bin-2017.2.13...
Scala是一种强大的多范式编程语言,它融合了函数式编程和面向对象编程的特点。IntelliJ IDEA是一款广受赞誉的Java开发集成环境,为开发者提供了高效、智能的代码编写体验。"scala-intellij-bin-0.41"是专门为...
`scala-intellij-bin-2018.3.5.zip` 和 `scala-intellij-bin-2018.3.6.zip` 是两个版本的Scala插件,分别适用于IntelliJ IDEA的2018.3.5和2018.3.6版本。这些插件是为了增强IDE对Scala语言的支持,提供代码高亮、...
scala eclipse插件.对应scala版本:2.10--2.11,对应eclipes版本:4.4--4.5. update site:http://download.scala-ide.org/sdk/lithium/e44/scala211/stable/site 下载地址:...
包含翻译后的API文档:scala-compiler-2.11.12-javadoc-API文档-中文(简体)版.zip; Maven坐标:org.scala-lang:scala-compiler:2.11.12; 标签:scala、lang、compiler、中文文档、jar包、java; 使用方法:解压...
5. **调试器**:支持Scala程序的调试,包括设置断点、步进执行、查看变量值等,帮助开发者理解代码运行过程。 6. **框架集成**:对于使用Scala构建的框架如Akka、Play Framework,插件通常会提供额外的支持,如模板...
包含翻译后的API文档:scala-xml_2.11-1.0.1-javadoc-API文档-中文(简体)版.zip; Maven坐标:org.scala-lang.modules:scala-xml_2.11:1.0.1; 标签:11、xml_2、scala、lang、modules、jar包、java、中文文档; ...
标签:library、scala、lang、jar包、java、API文档、中文版; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档内容。 人性化翻译,文档中的代码和结构保持不变,注释和说明精准翻译...
Scala是一种强大的多范式编程语言,它融合了函数式编程和面向对象编程的特点。IntelliJ IDEA是一款广受开发者喜爱的集成开发环境(IDE),它提供了丰富的功能来支持各种编程语言,包括Scala。"scala-intellij-bin-...