💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
当你在尝试一门新的语言时,可能不会过于关注程序出错的问题,但当真的去创造可用的代码时,就不能再忽视代码中的可能产生的错误和异常了。鉴于各种各样的原因,人们往往低估了语言对错误处理支持程度的重要性。 事实会表明,Scala 能够很优雅的处理此类问题,这一部分,我会介绍 Scala 基于 Try 的错误处理机制,以及这背后的原因。我将使用一个在 _Scala 2.10_ 新引入的特性,该特性向 _2.9.3_ 兼容,因此,请确保你的 Scala 版本不低于 _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 中那种需要使用按约定进行检查的错误编码的错误处理。相反,Scala 使用一个特定的类型来表示可能会导致异常的计算,这个类型就是 Try。 #### Try 的语义 解释 Try 最好的方式是将它与上一章所讲的 Option 作对比。 `Option[A]` 是一个可能有值也可能没值的容器,`Try[A]` 则表示一种计算:这种计算在成功的情况下,返回类型为 `A` 的值,在出错的情况下,返回 `Throwable` 。这种可以容纳错误的容器可以很轻易的在并发执行的程序之间传递。 Try 有两个子类型: 1. `Success[A]`:代表成功的计算。 1. 封装了 `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)` 是在 `Try` 的 `apply` 方法里执行的。 `apply` 方法不会捕获任何非致命的异常,仅仅返回一个包含相关异常的 Failure 实例。 因此, `parseURL("http://danielwestheide.com")` 会返回一个 `Success[URL]` ,包含了解析后的网址,而 `parseULR("garbage")` 将返回一个含有 `MalformedURLException` 的 `Failure[URL]`。 #### 使用 Try 使用 Try 与使用 Option 非常相似,在这里你看不到太多新的东西。 你可以调用 `isSuccess` 方法来检查一个 Try 是否成功,然后通过 `get` 方法获取它的值,但是,这种方式的使用并不多见,因为你可以用 `getOrElse` 方法给 Try 提供一个默认值: ~~~ val url = parseURL(Console.readLine("URL: ")) getOrElse new URL("http://duckduckgo.com") ~~~ 如果用户提供的 URL 格式不正确,我们就使用 DuckDuckGo 的 URL 作为备用。 #### 链式操作 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]` 返回,否则, `flatMap` 将 `Success[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 当然,你也可以对 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] ~~~ 当一个 Try 是 `Success` 时, `foreach` 允许你在被包含的元素上执行副作用,这种情况下,传递给 `foreach` 的函数只会执行一次,毕竟 Try 里面只有一个元素: ~~~ parseHttpURL("http://danielwestheide.com").foreach(println) ~~~ > 当 Try 是 Failure 时, `foreach` 不会执行,返回 `Unit` 类型。 ### for 语句中的 Try 既然 Try 支持 `flatMap` 、 `map` 、 `filter` ,能够使用 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,这时候,你应该想到模式匹配,也幸好, `Success` 和 `Failure` 都是样例类。 接着上面的例子,如果网页内容能顺利提取到,我们就展示它,否则,打印一个错误信息: ~~~ 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` 方法,`transform` 和 `recoverWith` 也都值得去看。 下一章,我们会探讨 Either,另外一种可以代表计算的类型,但它的可使用范围要比 Try 大的多。