一.概述

当你在尝试一门新的语言时,可能不会过于关注程序出错的问题, 但当真的去创造可用的代码时,就不能再忽视代码中的可能产生的错误和异常了。 鉴于各种各样的原因,人们往往低估了语言对错误处理支持程度的重要性。

事实会表明,Scala 能够很优雅的处理此类问题, 这一部分,我会介绍 Scala 基于 Try 的错误处理机制,以及这背后的原因。 我将使用一个在 Scala 2.10 新引入的特性,该特性向 2.9.3 兼容, 因此,请确保你的 Scala 版本不低于 2.9.3。

二.异常抛出与捕获

2.1 其他语言的错误处理机制

在介绍 Scala 错误处理的惯用法之前,我们先看看其他语言(如,Java,Ruby)的错误处理机制。 和这些语言类似,Scala 也允许你抛出异常:

case class Customer(age: Int)
class Cigarettes
case class UnderAgeException(message: String) extends Exception(message)
def buyCigarettes(customer: Customer): Cigarettes =
if (customer.age < 16)
throw UnderAgeException(s"Customer must be older than 16 but was ${customer.age}")
else new Cigarettes

被抛出的异常能够以类似 Java 中的方式被捕获,虽然是使用偏函数来指定要处理的异常类型。 此外,Scala 的 try/catch 是表达式(返回一个值),因此下面的代码会返回异常的消息:

val youngCustomer = Customer(15)
try {
buyCigarettes(youngCustomer)
"Yo, here are your cancer sticks! Happy smokin'!"
} catch {
case UnderAgeException(msg) => msg
}

2.2 函数式的错误处理

现在,如果代码中到处是上面的异常处理代码,那它很快就会变得丑陋无比,和函数式程序设计非常不搭。 对于高并发应用来说,这也是一个很差劲的解决方式,比如, 假设需要处理在其他线程执行的 actor 所引发的异常,显然你不能用捕获异常这种处理方式, 你可能会想到其他解决方案,例如去接收一个表示错误情况的消息。

一般来说,在 Scala 中,好的做法是通过从函数里返回一个合适的值来通知人们程序出错了。 别担心,我们不会回到 C 中那种需要使用按约定进行检查的错误编码的错误处理。 相反,Scala 使用一个特定的类型来表示可能会导致异常的计算,这个类型就是 Try。

Try 的语义

解释 Try 最好的方式是将它与 Option 作对比。

Option[A] 是一个可能有值也可能没值的容器, Try[A] 则表示一种计算: 这种计算在成功的情况下,返回类型为 A 的值,在出错的情况下,返回 Throwable 。 这种可以容纳错误的容器可以很轻易的在并发执行的程序之间传递。

Try 有两个子类型:

  • Success[A]:代表成功的计算。
  • 封装了 Throwable 的 Failure[A]:代表出了错的计算。

如果知道一个计算可能导致错误,我们可以简单的使用 Try[A] 作为函数的返回类型。 这使得出错的可能性变得很明确,而且强制客户端以某种方式处理出错的可能。

假设,需要实现一个简单的网页爬取器:用户能够输入想爬取的网页 URL, 程序就需要去分析 URL 输入,并从中创建一个 java.net.URL :

import scala.util.Try
import java.net.URL
def parseURL(url: String): Try[URL] = Try(new URL(url))

正如你所看到的,函数返回类型为 Try[URL]: 如果给定的 url 语法正确,这将是 Success[URL], 否则, URL 构造器会引发 MalformedURLException ,从而返回值变成 Failure[URL] 类型。

上例中,我们还用了 Try 伴生对象里的 apply 工厂方法,这个方法接受一个类型为 A 的 传名参数, 这意味着, new URL(url) 是在 Tryapply 方法里执行的。

apply 方法会捕获任何非致命的异常,返回一个包含相关异常的 Failure 实例。

因此, parseURL("http://danielwestheide.com") 会返回一个 Success[URL] ,包含了解析后的网址, 而 parseULR("garbage") 将返回一个含有 MalformedURLExceptionFailure[URL]

三. 使用 Try

3.1 初步使用 Try

使用 Try 与使用 Option 非常相似,在这里你看不到太多新的东西。

你可以调用 isSuccess 方法来检查一个 Try 是否成功,然后通过 get 方法获取它的值, 但是,这种方式的使用并不多见,因为你可以用 getOrElse 方法给 Try 提供一个默认值:

val url = parseURL(Console.readLine("URL: ")) getOrElse new URL("http://duckduckgo.com")

如果用户提供的 URL 格式不正确,我们就使用 DuckDuckGo 的 URL 作为备用。

3.2 链式操作

Try 最重要的特征是,它也支持高阶函数,就像 Option 一样。 在下面的示例中,你将看到,在 Try 上也进行链式操作,捕获可能发生的异常,而且代码可读性不错。

Mapping 和 Flat Mapping

将一个是 Success[A]Try[A] 映射到 Try[B] 会得到 Success[B] 。 如果它是 Failure[A] ,就会得到 Failure[B] ,而且包含的异常和 Failure[A] 一样。

parseURL("http://danielwestheide.com").map(_.getProtocol)
// results in Success("http")
parseURL("garbage").map(_.getProtocol)
// results in Failure(java.net.MalformedURLException: no protocol: garbage)

如果链接多个 map 操作,会产生嵌套的 Try 结构,这并不是我们想要的。 考虑下面这个返回输入流的方法:

import java.io.InputStream
def inputStreamForURL(url: String): Try[Try[Try[InputStream]]] = parseURL(url).map { u =>
Try(u.openConnection()).map(conn => Try(conn.getInputStream))
}

由于每个传递给 map 的匿名函数都返回 Try,因此返回类型就变成了 Try[Try[Try[InputStream]]]

这时候, flatMap 就派上用场了。 Try[A] 上的 flatMap 方法接受一个映射函数,这个函数类型是 (A) => Try[B]。 如果我们的 Try[A] 已经是 Failure[A] 了,那么里面的异常就直接被封装成 Failure[B] 返回, 否则, flatMapSuccess[A] 里面的值解包出来,并通过映射函数将其映射到 Try[B]

这意味着,我们可以通过链接任意个 flatMap 调用来创建一条操作管道,将值封装在 Success 里一层层的传递。

现在让我们用 flatMap 来重写先前的例子:

def inputStreamForURL(url: String): Try[InputStream] =
parseURL(url).flatMap { u =>
Try(u.openConnection()).flatMap(conn => Try(conn.getInputStream))
}

这样,我们就得到了一个 Try[InputStream], 它可以是一个 Failure,包含了在 flatMap 过程中可能出现的异常; 也可以是一个 Success,包含了最后的结果。

过滤器和 foreach

过滤器和 foreach

当然,你也可以对 Try 进行过滤,或者调用 foreach ,如果你已经学过 Option,对于这两个方法也不会陌生。

当一个 Try 已经是 Failure 了,或者传递给它的谓词函数返回假值,filter 就返回 Failure (如果是谓词函数返回假值,那 Failure 里包含的异常是 NoSuchException ), 否则的话, filter 就返回原本的那个 Success ,什么都不会变:

def parseHttpURL(url: String) = parseURL(url).filter(_.getProtocol == "http")
parseHttpURL("http://apache.openmirror.de") // results in a Success[URL]
parseHttpURL("ftp://mirror.netcologne.de/apache.org") // results in a Failure[URL]

当一个 TrySuccess 时, foreach 允许你在被包含的元素上执行副作用, 这种情况下,传递给 foreach 的函数只会执行一次,毕竟 Try 里面只有一个元素:

 parseHttpURL("http://danielwestheide.com").foreach(println)

当 Try 是 Failure 时, foreach 不会执行,返回 Unit 类型。

for 语句中的 Try

既然 Try 支持 flatMapmapfilter ,能够使用 for 语句也是理所当然的事情, 而且这种情况下的代码更可读。 为了证明这一点,我们来实现一个返回给定 URL 的网页内容的函数:

import scala.io.Source
def getURLContent(url: String): Try[Iterator[String]] =
for {
url <- parseURL(url)
connection <- Try(url.openConnection())
is <- Try(connection.getInputStream)
source = Source.fromInputStream(is)
} yield source.getLines()

这个方法中,有三个可能会出错的地方,但都被 Try 给涵盖了。 第一个是我们已经实现的 parseURL 方法, 只有当它是一个 Success[URL] 时,我们才会尝试打开连接,从中创建一个新的 InputStream 。 如果这两步都成功了,我们就 yield 出网页内容,得到的结果是 Try[Iterator[String]]

当然,你可以使用 Source#fromURL 简化这个代码,并且,这个代码最后没有关闭输入流, 这都是为了保持例子的简单性,专注于要讲述的主题。

在这个例子中,Source#fromURL可以这样用:

import scala.io.Source
def getURLContent(url: String): Try[Iterator[String]] =
for {
url <- parseURL(url)
source = Source.fromURL(url)
} yield source.getLines()

用 is.close() 可以关闭输入流。

模式匹配

代码往往需要知道一个 Try 实例是 Success 还是 Failure,这时候,你应该想到模式匹配, 也幸好, SuccessFailure 都是样例类。

接着上面的例子,如果网页内容能顺利提取到,我们就展示它,否则,打印一个错误信息:

import scala.util.Success
import scala.util.Failure
getURLContent("http://danielwestheide.com/foobar") match {
case Success(lines) => lines.foreach(println)
case Failure(ex) => println(s"Problem rendering URL content: ${ex.getMessage}")
}
从故障中恢复

如果想在失败的情况下执行某种动作,没必要去使用 getOrElse, 一个更好的选择是 recover ,它接受一个偏函数,并返回另一个 Try。 如果 recover 是在 Success 实例上调用的,那么就直接返回这个实例,否则就调用偏函数。 如果偏函数为给定的 Failure 定义了处理动作, recover 会返回 Success ,里面包含偏函数运行得出的结果。

下面是应用了 recover 的代码:

import java.net.MalformedURLException
import java.io.FileNotFoundException
val content = getURLContent("garbage") recover {
case e: FileNotFoundException => Iterator("Requested page does not exist")
case e: MalformedURLException => Iterator("Please make sure to enter a valid URL")
case _ => Iterator("An unexpected error has occurred. We are so sorry!")
}

现在,我们可以在返回值 content 上安全的使用 get 方法了,因为它一定是一个 Success。 调用 content.get.foreach(println) 会打印 Please make sure to enter a valid URL。

四. 总结

Scala 的错误处理和其他范式的编程语言有很大的不同。 Try 类型可以让你将可能会出错的计算封装在一个容器里,并优雅的去处理计算得到的值。 并且可以像操作集合和 Option 那样统一的去操作 Try。

Try 还有其他很多重要的方法,鉴于篇幅限制,这一章并没有全部列出,比如 orElse 方法, transformrecoverWith 也都值得去看。

文章转自:https://windor.gitbooks.io/beginners-guide-to-scala/content/chp6-error-handling-with-try.html

[转] Scala Try 与错误处理的更多相关文章

  1. 安装eclipse scala插件

    1.安装eclipse插件,依次点击Help->Eclipse Marketplace 2.输入scala,点击go,进行搜索 3,出现了Scala IDE4.7X,点击右下方的Install进 ...

  2. eclipse安装scala环境

    1.安装eclipse插件,依次点击Help->Eclipse Marketplace 2.输入scala,点击go,进行搜索 3,出现了Scala IDE4.7X,点击右下方的Install进 ...

  3. (数据科学学习手札49)Scala中的模式匹配

    一.简介 Scala中的模式匹配类似Java中的switch语句,且更加稳健,本文就将针对Scala中模式匹配的一些基本实例进行介绍: 二.Scala中的模式匹配 2.1 基本格式 Scala中模式匹 ...

  4. 2020寒假 05 ——eclipse安装scala环境

    在eclipse中安装Scala环境 1安装eclipse插件步骤,点击help,选择Eclipse Marketplace 2.输入Scala,点击go 3.选择搜索到的Scala IDE 4.7. ...

  5. Spark UI (基于Yarn) 分析与定制

    转载自:https://yq.aliyun.com/articles/60194 摘要: 这篇文章的主旨在于让你了解Spark UI体系,并且能够让你有能力对UI进行一些定制化增强.在分析过程中,你也 ...

  6. 航空概论(历年资料,引之百度文库,PS:未调格式,有点乱)

    航空航天尔雅 选择题1. 已经实现了<天方夜谭>中的飞毯设想.—— A——美国2. 地球到月球大约—— C 38 万公里3. 建立了航空史上第一条定期空中路线—— B——德国4. 对于孔明 ...

  7. spark-shell和scala错误

    运行spark-shell 或者scala命令,出现以下错误: Welcome to Scala version 2.10.6 (Java HotSpot(TM) 64-Bit Server VM, ...

  8. 错误: 找不到或无法加载主类 scala.tools.nsc.MainGenericRunner

    错误: 找不到或无法加载主类 scala.tools.nsc.MainGenericRunner 原因: Sacala安装路径中包含空格.

  9. scala IDE错误:..is cross-compiled with incompatible version....

    下午scala工程出现如下错误: 搜索这个问题,没有找到答案. 直接去官网查看http://scala-ide.org/docs/current-user-doc/faq/index.html,发现了 ...

随机推荐

  1. Docker 快速入门之 Dockerfile

    在 Docker 中我们可以从名为 Dockerfile 的文件中读取指令并且自动构建镜像.在本文中,将介绍 Dockerfile 的基本语法以及基本知识. Dockerfile 是什么 Docker ...

  2. LeetCode算法扫题系列83

    原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/9104582.html LeetCode算法第83题(难度:简单) 题目:给定一个排序链表 ...

  3. Linux核心命令

    Linux核心命令 strace(查看系统调用的一个过程) 例:strace cat /test.txt netstat perf top pidstat mpstat dstat vmstat sl ...

  4. YARN集群的mapreduce测试(六)

    两张表链接操作(分布式缓存): ----------------------------------假设:其中一张A表,只有20条数据记录(比如group表)另外一张非常大,上亿的记录数量(比如use ...

  5. C#调用C++(QT5.5.1项目)的C++/CLI(CLR项目)项目技术笔记

    导航 1.编译环境 2.项目配置 1.设置附加包含目录 2.设置附加库目录 3.设置附加依赖项 3.CLR中各种定义 1.接口定义 2.类定义 3.枚举定义 4.属性定义 4.CLR中各种使用 1.类 ...

  6. 《Office 365开发入门指南》上市说明和读者服务

    写在最开始的话 拙作<Office 365开发入门指南>上周开始已经正式在各大书店.在线商城上市,欢迎对Office 365的开发.生态感兴趣的开发者.项目经理.产品经理参考本书,全面了解 ...

  7. WPF window 子窗口反馈效果(抖动/阴影渐变)

    当设置了owner的子窗口显示后,点击子窗口外部,需要一种反馈机制(反馈动画). 实现: 1.触发源 每次点击子窗口外部,即母窗口时,事件捕捉如下 HwndSource hwndSource = Pr ...

  8. 4. explain简介

    一.是什么 使用 explain 关键字可以模拟优化器执行SQl查询语句,从而知道 mysql 是如何处理你的sql语句的.分析你的查询语句或是表的结构的性能瓶颈. 二.能干嘛 表的读取顺序 数据读取 ...

  9. Fzreo matlab

    fzero Root of nonlinear function collapse all in page Syntax x = fzero(fun,x0) example x = fzero(fun ...

  10. JQuery禁止/恢复按钮readonly和disabled小结

    禁止/恢复按钮 其实就是更改 <input> 的属性. 禁用 id 为 btn 的按钮:  代码如下 复制代码 $("#btn").attr({"disabl ...