Scala 初学者指南 类型Option学习
2016-04-07 00:00
609 查看
摘要: 非常好的scala教程
可能你已经见过它在
值
Clojure 对待
Scala 试图通过摆脱
在类型层面上指出一个值是否存在,使用你的代码的开发者(也包括你自己)就会被编译器强制去处理这种可能性, 而不能依赖值存在的偶然性。
或者,在知道值缺失的情况下,直接使用
然而,在实际工作中,你不可避免的要去操作一些 Java 库, 或者是其他将
想象一下,你正在为某个创业公司工作,要做的第一件事情就是实现一个用户的存储库, 要求能够通过唯一的用户 ID 来查找他们。 有时候请求会带来假的 ID,这种情况,查找方法就需要返回
现在,假设从
一个办法就是通过
这和 Guava 库 中的
你应该尽可能远离这种访问方式!
请注意,作为
或者,你想删除重复的
你可能已经发现用模式匹配处理
不过在 Option 上使用模式确实是有一个相当优雅的方式, 在下面的 for 语句一节中,你就会学到。
前文我提到过,
虽然在类型层次上,
那么这又能让你做什么呢?
如果这个 Option 是一个
正如你可以将
如果将
让我们得到一个可能不存在的用户的年龄:
所生成的
这样想:你有一个装有
既然可以
现在结果就变成了
要理解这是什么原理,让我们看看当
如果我们使用
现在回到
如果只是
记住这一点,然后再去看看
假如我们想得到一个用户的性别,可以这样使用 for 语句:
可能你已经知道,这样的 for 语句等同于嵌套的
如果我们想返回所有用户的性别(当然,如果用户设置了性别),可以遍历用户,yield 其性别:
重写之前的例子:
在生成器左侧使用
一个很好的使用案例是资源查找:对多个不同的地方按优先级进行搜索。 下面的例子中,我们首先搜索config 文件夹,并调用
如果想链接多个选项,而不仅仅是两个,使用
这一章最重要的一点是:列表、集合、映射、Option,以及之后你会见到的其他数据类型, 它们都有一个非常统一的使用方式,这种使用方式既强大又优雅。
下一章,你将学习 Scala 错误处理的惯用法。
英文原文:
http://danielwestheide.com/blog/2013/01/09/the-neophytes-guide-to-scala-part-8-welcome-to-the-future.html
类型 Option
前几章,我们讨论了许多相当先进的技术,尤其是模式匹配和提取器。 是时候来看一看 Scala 另一个基本特性了: Option 类型。可能你已经见过它在
MapAPI 中的使用;在实现自己的提取器时,我们也用过它, 然而,它还需要更多的解释。 你可能会想知道它到底解决什么问题,为什么用它来处理缺失值要比其他方法好, 而且可能你还不知道该怎么在你的代码中使用它。 这一章的目的就是消除这些问号,并教授你作为一个新手所应该了解的
Option知识。
基本概念
Java 开发者一般都知道NullPointerException(其他语言也有类似的东西), 通常这是由于某个方法返回了
null,但这并不是开发者所希望发生的,代码也不好去处理这种异常。
值
null通常被滥用来表征一个可能会缺失的值。 不过,某些语言以一种特殊的方法对待
null值,或者允许你安全的使用可能是
null的值。 比如说,Groovy 有 安全运算符(Safe Navigation Operator) 用于访问属性, 这样
foo?.bar?.baz不会在
foo或
bar是
null时而引发异常,而是直接返回
null, 然而,Groovy 中没有什么机制来强制你使用此运算符,所以如果你忘记使用它,那就完蛋了!
Clojure 对待
nil基本上就像对待空字符串一样。 也可以把它当作列表或者映射表一样去访问,这意味着,
nil在调用层级中向上冒泡。 很多时候这样是可行的,但有时会导致异常出现在更高的调用层级中,而那里的代码没有对
nil加以考虑。
Scala 试图通过摆脱
null来解决这个问题,并提供自己的类型用来表示一个值是可选的(有值或无值), 这就是
Option[A]特质。
Option[A]是一个类型为
A的可选值的容器: 如果值存在,
Option[A]就是一个
Some[A],如果不存在,
Option[A]就是对象
None。
在类型层面上指出一个值是否存在,使用你的代码的开发者(也包括你自己)就会被编译器强制去处理这种可能性, 而不能依赖值存在的偶然性。
Option是强制的!不要使用
null来表示一个值是缺失的。
创建 Option
通常,你可以直接实例化Some样例类来创建一个 Option 。
val greeting: Option[String] = Some("Hello world")
或者,在知道值缺失的情况下,直接使用
None对象:
val greeting: Option[String] = None
然而,在实际工作中,你不可避免的要去操作一些 Java 库, 或者是其他将
null作为缺失值的JVM 语言的代码。 为此,
Option伴生对象提供了一个工厂方法,可以根据给定的参数创建相应的
Option:
val absentGreeting: Option[String] = Option(null) // absentGreeting will be Noneval presentGreeting: Option[String] = Option("Hello!") // presentGreeting will be Some("Hello!")
使用 Option
目前为止,所有的这些都很简洁,不过该怎么使用 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]实例,并需要拿它做点什么,该怎么办呢?
一个办法就是通过
isDefined方法来检查它是否有值。 如果有,你就可以用
get方法来获取该值:
val user1 = UserRepository.findById(1) if (user1.isDefined) { println(user1.get.firstName) } // will print "John"
这和 Guava 库 中的
Optional使用方法类似。 不过这种使用方式太过笨重,更重要的是,使用
get之前, 你可能会忘记用
isDefined做检查,这会导致运行期出现异常。 这样一来,相对于
null,使用
Option并没有什么优势。
你应该尽可能远离这种访问方式!
提供一个默认值
很多时候,在值不存在时,需要进行回退,或者提供一个默认值。 Scala 为Option提供了
getOrElse方法,以应对这种情况:
val user = User(2, "Johanna", "Doe", 30, None) println("Gender: " + user.gender.getOrElse("not specified")) // will print "not specified"
请注意,作为
getOrElse参数的默认值是一个 传名参数 , 这意味着,只有当这个
Option确实是
None时,传名参数才会被求值。 因此,没必要担心创建默认值的代价,它只有在需要时才会发生。
模式匹配
Some是一个样例类,可以出现在模式匹配表达式或者其他允许模式出现的地方。 上面的例子可以用模式匹配来重写:
val user = User(2, "Johanna", "Doe", 30, None) user.gender match { case Some(gender) => println("Gender: " + gender) case None => println("Gender: not specified") }
或者,你想删除重复的
println语句,并重点突出模式匹配表达式的使用:
val user = User(2, "Johanna", "Doe", 30, None) val gender = user.gender match { case Some(gender) => gender case None => "not specified" } println("Gender: " + gender)
你可能已经发现用模式匹配处理
Option实例是非常啰嗦的,这也是它非惯用法的原因。 所以,即使你很喜欢模式匹配,也尽量用其他方法吧。
不过在 Option 上使用模式确实是有一个相当优雅的方式, 在下面的 for 语句一节中,你就会学到。
作为集合的 Option
到目前为止,你还没有看见过优雅使用 Option 的方式吧。下面这个就是了。前文我提到过,
Option是类型
A的容器,更确切地说,你可以把它看作是某种集合, 这个特殊的集合要么只包含一个元素,要么就什么元素都没有。
虽然在类型层次上,
Option并不是 Scala 的集合类型, 但,凡是你觉得 Scala 集合好用的方法,
Option也有, 你甚至可以将其转换成一个集合,比如说
List。
那么这又能让你做什么呢?
执行一个副作用
如果想在 Option 值存在的时候执行某个副作用,foreach方法就派上用场了:
UserRepository.findById(2).foreach(user => println(user.firstName)) // prints "Johanna"
如果这个 Option 是一个
Some,传递给
foreach的函数就会被调用一次,且只有一次; 如果是
None,那它就不会被调用。
执行映射
Option表现的像集合,最棒的一点是, 你可以用它来进行函数式编程,就像处理列表、集合那样。
正如你可以将
List[A]映射到
List[B]一样,你也可以映射
Option[A]到
Option[B]: 如果
Option[A]实例是
Some[A]类型,那映射结果就是
Some[B]类型;否则,就是
None。
如果将
Option和
List做对比 ,那
None就相当于一个空列表: 当你映射一个空的
List[A],会得到一个空的
List[B], 而映射一个是
None的
Option[A]时,得到的
Option[B]也是
None。
让我们得到一个可能不存在的用户的年龄:
val age = UserRepository.findById(1).map(_.age) // age is Some(32)
Option 与 flatMap
也可以在gender上做
map操作:
val gender = UserRepository.findById(1).map(_.gender) // gender is an Option[Option[String]]
所生成的
gender类型是
Option[Option[String]]。这是为什么呢?
这样想:你有一个装有
User的
Option容器,在容器里面,你将
User映射到
Option[String](
User类上的属性
gender是
Option[String]类型的)。 得到的必然是嵌套的 Option。
既然可以
flatMap一个
List[List[A]]到
List[B], 也可以
flatMap一个
Option[Option[A]]到
Option[B],这没有任何问题: Option 提供了
flatMap方法。
val gender1 = UserRepository.findById(1).flatMap(_.gender) // gender is Some("male")val gender2 = UserRepository.findById(2).flatMap(_.gender) // gender is Noneval gender3 = UserRepository.findById(3).flatMap(_.gender) // gender is None
现在结果就变成了
Option[String]类型, 如果
user和
gender都有值,那结果就会是
Some类型,反之,就得到一个
None。
要理解这是什么原理,让我们看看当
flatMap一个
List[List[A]]时,会发生什么? (要记得, Option 就像一个集合,比如列表)
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,内部列表中的所有元素会被转换成一个扁平的字符串列表。 显然,如果内部列表是空的,则不会有任何东西留下。
现在回到
Option类型,如果映射一个由
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,那结果类型还是
List[Option[String]]。 而使用
flatMap时,内部集合的元素就会被放到一个扁平的列表里: 任何一个
Some[String]里的元素都会被解包,放入结果集中; 而原列表中的
None值由于不包含任何元素,就直接被过滤出去了。
记住这一点,然后再去看看
faltMap在
Option身上做了什么。
过滤 Option
也可以像过滤列表那样过滤 Option: 如果选项包含有值,而且传递给filter的谓词函数返回真,
filter会返回
Some实例。 否则(即选项没有值,或者谓词函数返回假值),返回值为
None。
UserRepository.findById(1).filter(_.age > 30) // None, because age is <= 30UserRepository.findById(2).filter(_.age > 30) // Some(user), because age is > 30UserRepository.findById(3).filter(_.age > 30) // None, because user is already None
for 语句
现在,你已经知道 Option 可以被当作集合来看待,并且有map、
flatMap、
filter这样的方法。 可能你也在想 Option 是否能够用在 for 语句中,答案是肯定的。 而且,用 for 语句来处理 Option 是可读性最好的方式,尤其是当你有多个
map、
flatMap、
filter调用的时候。 如果只是一个简单的
map调用,那 for 语句可能有点繁琐。
假如我们想得到一个用户的性别,可以这样使用 for 语句:
for { user <- UserRepository.findById(1) gender <- user.gender } yield gender // results in Some("male")
可能你已经知道,这样的 for 语句等同于嵌套的
flatMap调用。 如果
UserRepository.findById返回
None,或者
gender是
None, 那这个 for 语句的结果就是
None。 不过这个例子里,
gender含有值,所以返回结果是
Some类型的。
如果我们想返回所有用户的性别(当然,如果用户设置了性别),可以遍历用户,yield 其性别:
for { user <- UserRepository.findAll gender <- user.gender } yield gender// result in List("male")
在生成器左侧使用
也许你还记得,前一章曾经提到过, for 语句中生成器的左侧也是一个模式。 这意味着也可以在 for 语句中使用包含选项的模式。重写之前的例子:
for { User(_, _, _, _, Some(gender)) <- UserRepository.findAll } yield gender
在生成器左侧使用
Some模式就可以在结果集中排除掉值为
None的元素。
链接 Option
Option 还可以被链接使用,这有点像偏函数的链接: 在 Option 实例上调用orElse方法,并将另一个 Option 实例作为传名参数传递给它。 如果一个 Option 是
None,
orElse方法会返回传名参数的值,否则,就直接返回这个 Option。
一个很好的使用案例是资源查找:对多个不同的地方按优先级进行搜索。 下面的例子中,我们首先搜索config 文件夹,并调用
orElse方法,以传递备用目录:
case class Resource(content: String)val resourceFromConfigDir: Option[Resource] = Noneval resourceFromClasspath: Option[Resource] = Some(Resource("I was found on the classpath"))val resource = resourceFromConfigDir orElse resourceFromClasspath
如果想链接多个选项,而不仅仅是两个,使用
orElse会非常合适。 不过,如果只是想在值缺失的情况下提供一个默认值,那还是使用
getOrElse吧。
总结
在这一章里,你学到了有关 Option 的所有知识, 这有利于你理解别人的代码,也有利于你写出更可读,更函数式的代码。这一章最重要的一点是:列表、集合、映射、Option,以及之后你会见到的其他数据类型, 它们都有一个非常统一的使用方式,这种使用方式既强大又优雅。
下一章,你将学习 Scala 错误处理的惯用法。
英文原文:
http://danielwestheide.com/blog/2013/01/09/the-neophytes-guide-to-scala-part-8-welcome-to-the-future.html
相关文章推荐
- Hadoop之MapReduce自定义二次排序流程实例详解
- Hadoop Mapreduce分区、分组、二次排序过程详解
- Linux内核如何装载和启动一个可执行程序
- Activity有四种加载模式:standard(默认), singleTop, singleTask和 singleInstance
- 初识openstack
- 20135202闫佳歆--week 7 Linux内核如何装载和启动一个可执行程序--实验及总结
- 启动文件系统Kernel panic - not syncing: Attempted to kill init! 报错
- Nginx
- 例解 Linux 下 Make 命令
- Linux高性能服务器编程笔记1
- Linux 升级 Python 至 3.x
- Linux 升级 Python 至 3.x
- Opengl新手入门用什么书?
- 为什么需要架构图,怎么画?
- Linux 常用命令
- Linux下安装JDK1.8
- Linux内核分析第七周总结
- hadoop-2.6.0伪分布式单机安装傻瓜教程
- Could not find Developer Disk Image
- Hadoop学习笔记(1):WordCount程序的实现与总结