`
thomassun
  • 浏览: 27544 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

Scala新手指南中文版 -第六篇 Error Handling with Try(用Try来处理错误)

阅读更多

译者注:原文出处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,她是封装代码异常的另一种方案,不过除了进行错误处理,它还用在其它场景。

作者:Daniel Westheide,2012.12.26

 

分享到:
评论

相关推荐

    scala-xml_2.12-1.0.6-API文档-中文版.zip

    包含翻译后的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; 使用方法...

    flink-scala_2.12-1.14.3-API文档-中文版.zip

    包含翻译后的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-compiler-2.12.7-API文档-中文版.zip

    包含翻译后的API文档:scala-compiler-2.12.7-javadoc-API文档-中文(简体)版.zip; Maven坐标:org.scala-lang:scala-compiler:2.12.7; 标签:scala、lang、compiler、中文文档、jar包、java; 使用方法:解压翻译...

    scala-compiler-2.11.8-API文档-中英对照版.zip

    包含翻译后的API文档:scala-compiler-2.11.8-javadoc-API文档-中文(简体)-英语-对照版.zip; Maven坐标:org.scala-lang:scala-compiler:2.11.8; 标签:scala、lang、compiler、中英对照文档、jar包、java; 使用...

    scala-parser-combinators-2.11-1.0.4-API文档-中文版.zip

    标签:11、parser、scala、combinators_2、lang、modules、jar包、java、API文档、中文版; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档内容。 人性化翻译,文档中的代码和结构...

    scala-intellij-bin-2016.3.9

    总的来说,"scala-intellij-bin-2016.3.9"插件是Scala开发者在IntelliJ IDEA中高效工作的重要工具,它通过丰富的特性集和与IDE的紧密集成,使得Scala编程变得更加顺畅和愉快。如果你是Scala的爱好者或者正在从事...

    scala-reflect-2.11.8-API文档-中英对照版.zip

    包含翻译后的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-intellij-bin-0.41

    Scala是一种强大的多范式编程语言,它融合了函数式编程和面向对象编程的特点。IntelliJ IDEA是一款广受赞誉的Java开发集成环境,为开发者提供了高效、智能的代码编写体验。"scala-intellij-bin-0.41"是专门为...

    scala-compiler-2.11.12-API文档-中文版.zip

    包含翻译后的API文档:scala-compiler-2.11.12-javadoc-API文档-中文(简体)版.zip; Maven坐标:org.scala-lang:scala-compiler:2.11.12; 标签:scala、lang、compiler、中文文档、jar包、java; 使用方法:解压...

    scala插件 scala-intellij-bin-2018.3.5.zip scala-intellij-bin-2018.3.6.zip

    如果下载的zip文件中包含`readme.txt`,通常这个文件会包含关于插件的安装指南、更新日志或者使用注意事项。务必阅读此文件,以获取更详细的安装和使用信息。 对于Spark开发,Scala插件同样提供了极大的便利。Spark...

    scala-intellij-bin-2016.3.1.zip

    总结来说,"scala-intellij-bin-2016.3.1.zip"是一个专为Scala开发定制的IntelliJ IDEA版本,提供了丰富的代码编辑、调试和项目管理功能,是Scala开发者不可或缺的工具。通过这个插件,开发者可以充分利用Scala语言...

    scala-intellij-bin-2017.2.13

    Scala是一种强大的多范式编程语言,它融合了面向对象和函数式编程的特性。IntelliJ IDEA是一款广受赞誉的Java集成开发环境(IDE),它为开发者提供了丰富的工具和功能来提升开发效率。"Scala-intellij-bin-2017.2.13...

    scala-intellij-bin-2017.2.6

    "scala-intellij-bin-2017.2.6" 是一个特定版本的Scala插件,适用于IntelliJ IDEA,它提供了对Scala语言的全面支持,包括语法高亮、代码补全、错误检查以及调试功能。 这个插件的版本号"2017.2.6"表明它是2017年第...

    scala-xml_2.11-1.0.1-API文档-中文版.zip

    包含翻译后的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、中文文档; ...

    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-SDK-4.7.0-vfinal-2.12-linux.gtk.x86_64.tar.gz

    scala-library-2.11.8-API文档-中文版.zip

    标签:library、scala、lang、jar包、java、API文档、中文版; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档内容。 人性化翻译,文档中的代码和结构保持不变,注释和说明精准翻译...

    scala-intellij-bin-2020.2.3.zip

    Scala是一种强大的多范式编程语言,它融合了面向对象和函数式编程的概念。IntelliJ IDEA是一款著名的集成开发环境(IDE),广泛用于Java、Scala和其他 JVM 语言的开发。"scala-intellij-bin-2020.2.3.zip" 是一个...

    scala-intellij-bin-2019.1.2.zip

    IntelliJ IDEA是一款广受开发者喜爱的集成开发环境(IDE),它提供了丰富的功能来支持各种编程语言,包括Scala。"scala-intellij-bin-2019.1.2.zip"这个压缩包就是针对IntelliJ IDEA的Scala插件,版本为2019.1.2,...

    scala-intellij-bin-2018.3.2.zip

    scala-intellij-bin-2018.3.2.zip插件,亲测可用!!!scala-intellij-bin-2018.3.2.zip插件,亲测可用!!!scala-intellij-bin-2018.3.2.zip插件,亲测可用!!!

    scala-compiler-2.11.0-API文档-中文版.zip

    包含翻译后的API文档:scala-compiler-2.11.0-javadoc-API文档-中文(简体)版.zip; Maven坐标:org.scala-lang:scala-compiler:2.11.0; 标签:scala、lang、compiler、jar包、java、中文文档; 使用方法:解压翻译...

Global site tag (gtag.js) - Google Analytics