译者注:原文出处http://danielwestheide.com/blog/2012/12/26/the-neophytes-guide-to-scala-part-6-error-handling-with-try.html,翻译:Thomas
在新语言的学习阶段,你通常不会去想如果执行代码出了问题该如何处理。一旦你想要开发一个真实产品时,就必须认真面对错误和异常处理了。由于各种个语言对这方面支持程度的重要性有时候被认人忽视了。
Scala在设计之初就考虑了如何优雅的应对错误场景。在本篇中,我会介绍Scala以Try类型为基础的错误处理机制及内在原理。后面会用到Scala2.10才有的一些特性,2.9.3里也移植了这些特性,所以请确保你的实验环境是在2.9.3或以上版本。
抛出和捕捉异常
在介绍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 }
函数式的错误处理
在你的代码中处处出现这种“传统”的错误处理代码会让人觉得很难看,并且这也不算是函数式编程。并且当程序有很多并发代码时,这也不是个好的方法,比如说你就没法捕捉到执行在另一个线程的一个Actor抛出的异常 - 你可能希望收到一个表示异常/错误情况的消息。
在Scala里,首选的做法是从函数返回一个适当的值来表示发生了一个错误,不必担心,我们并没有回到C语言风格的错误处理,在C语言里,函数和调用方通过约定的错误代码来传递异常。在Scala里我们用一个特别的类型来表示一个可能会发生异常的计算。
在本篇,我们专注于从Scala2.10(2.9.3里也引进了)引进来的类型,Try。有一个叫Either的类似类型也挺有用,会在下一篇介绍到。
Try的含义
解释Try的含义的最好方法是拿Option类型的含义来参考。Option[A]表示一个A类型的值可能存在或不存在,那么Try[A]表示的是一段代码当成功执行时返回A类型的值,或者当错误发生时返回一个Throwable。这种包含可能存在错误的类型的实例可以很容易的在你程序中各个并发代码中互相传递。
Try有两个形态:如果一个Try[A]实例表示计算成功,它会是一个Success[A]的实例,包含着A类型的值,另一个形态是当代码出错时,它是一个Failure[A]的实例,包含一个Throwable,表示一个异常或其它类型的错误。
如果我们确切的知道一段代码可能会出错,我们可以让这个函数返回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在语法上是个URL,那么返回的是
Success[URL],如果URL构造器抛出MalformedURLException,返回类型将是Failure[URL]。
我们用了Try的伙伴对象的apply工厂方法,这方法输入一个by-name方式的URL类型的参数。在我们的例子中,new URL(url)
只在Try的apply方法里执行,这方法里执行的代码抛出的任何non-fatal异常都会被捕捉并封装在Failure 类型中返回。
因此,parseURL("http://danielwestheide.com")
会返回包含相应URL实例的Success[URL]
,而parseURL("garbage")会返回包含
MalformedURLException的Failure[URL]。
使用Try值
使用Try实例和使用Option类型非常相似。因此这里就没有太多惊喜了。
你可以呼叫isSuccess方法来判断Try是否成功,然后如果成功就调用它的get方法(这和上篇的Option的isDefined相似)。但是相信我,没有太多情况让你想要这样做。
也可以用getOrElse来传递一个默认值,如果Try是一个Failure时将返回这默认值:
val url = parseURL(Console.readLine("URL: ")) getOrElse new URL("http://duckduckgo.com")
如果用户提供的URL格式有错误,我们就用后备的URL,DuckDuckGo。
串联操作
Try类型的一个最重要的特性就是,和Option一样,它也支持你在所有集合类型里看到的高阶方法。在下面的例子中你会看到,这让你以一种可读性很强的方式串联很多Try并且捕获其中任何的异常。
Map和flat map
把一个为Success[A]
的Try[A] map到
Try[B]的结果
是Success[B]
实例。
如果实例为Failure[A]
, 那么Try[B]的map结果就是
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
结构,者往往不是你想要的。考虑下面的方法,该方法从给定的URL中获取内容的InputStream:
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],如果Try[A]是一个Success[A],flatMap则会解包出A并且返回一个Try[B]。
这意味着我们通过串联任意多个flatMap构造一个管道来进行一系列的计算,这些计算需要封装在Success中得值,管道中的任何一个操作丢出封装在Failure中的异常,就意味着最终结果会是Failure。我们用flatMap来重写一下inputStreamForURL方法:
def inputStreamForURL(url: String): Try[InputStream] = parseURL(url).flatMap { u => Try(u.openConnection()).flatMap(conn => Try(conn.getInputStream)) }现在我们会得到
Try[InputStream]的结果,如果任意一个flatMap中得方法失败,结果是封装在Failure中的异常
, 反之是封装在Success中得InputStream。
过滤和foreach
你还可以过滤一个Try或者呼叫它的foreach方法。这两个的用法和你在前面Option中学到的基本一样。
当Try是一个Failure或过滤方法返回false时,filter方法返回一个Failure。如果呼叫一个Success的filter方法并且过滤方法返回true,那么原始的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]仅当Try是Success时,传递给foreach方法的函数才被调用,foreach方法让你执行一个副作用。Success所包含的值被传递给foreach的函数参数,该函数被执行一次:
parseHttpURL("http://danielwestheide.com").foreach(println)
For语句
因为Try有提供flatMap,map以及filter的方法,那就意味着你可以用For语句的写法来串联Try实例的操作,For语句通常提供更好的可读性。我们用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方法,它返回Try[URL],只有当它返回Success[URL],我们才去尝试打开一个连接并构造一个新的inputStream。如果打开连接和构造inputStream都成功了,我们继续进行,最终产生网页的所有行。用这样高效的方式调用flatMap,最终结果会是Try[Iterator[String]]。
请注意上面的代码有两处可改善的,首先整个功能可以简化成只调用Source.fromURL即可,另外我们在程序结束时没有关闭所构造的inputStream,我还是这样写代码是想把焦点放在我们应该关注的方面,即Try。
模式匹配
在你的代码里,你可能经常需要知道从一个函数返回的Try实例代表着成功还是失败,并据此执行不同的代码分支,这时候你需要模式匹配,因为Success和Failure都是case class,所以这很容易实现。
我们想要把网页的内容显示出来,或者如果中间出现异常就打印一个错误信息:
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}") }如果你想要为Failure的场景设定一个默认行为,无需使用getOrElse,替代的方式是用recover方法,该方法需要传入一个偏函数,并返回另一个Try,如果recover被Success实例呼叫,拿它直接返回该实例,否则,返回包在Success里的偏函数的返回值。
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方法以获取 Try[Iterator[String]]里包着的结果了。调用 content.get.foreach(println)将会输出“ Please make sure to enter a valid URL”。
结论
Scala里的惯用的错误处理机制和其它的语言如Java或Ruby差别很大。Try类型让你可以封装代码计算结果的出错并且可以以一种优雅的方式将计算结果作串联操作。你可以像用集合或Option一样的方式来进行错误处理。
限于篇幅,我没有介绍Try的所有方法。像Option一样,Try也有orElse方法。transform和recoveryWith方法也值得一看,我建议你去查看Scala的文档并试着写写代码。
在下一篇,我们会接触到Either,她是封装代码异常的另一种方案,不过除了进行错误处理,它还用在其它场景。