52

我正在关注 Coursera 上的 Scala 课程。我也开始阅读 Odersky 的 Scala 书。

我经常听到的是,在函数式语言中抛出异常并不是一个好主意,因为它会破坏控制流,我们通常会返回带有失败或成功的 Either。Scala 2.10 似乎也将提供朝着这个方向发展的 Try。

但是在本书和课程中,Martin Odersky 似乎并没有说(至少现在)异常是不好的,他经常使用它们。我还注意到方法 assert / require ...

最后我有点困惑,因为我想遵循最佳实践,但它们并不清楚,而且语言似乎是双向的......

有人可以解释我在这种情况下应该使用什么吗?

4

2 回答 2

50

基本准则是对真正特殊的事情使用例外**。对于“普通”失败,最好使用Optionor Either。如果您正在与 Java 交互,当有人以错误的方式打喷嚏时会引发异常,您可以使用它Try来保证自己的安全。

让我们举一些例子。

假设您有一个从地图中获取某些东西的方法。会出什么问题?好吧,像段错误*堆栈溢出这样的戏剧性和危险的事情,或者像元素这样的预期没有找到。您会让segfault堆栈溢出引发异常,但如果您只是找不到元素,为什么不返回 anOption[V]而不是值或异常(或null)?

现在假设您正在编写一个用户应该输入文件名的程序。现在,如果您不只是在出现问题时立即放弃该程序,那么您可以Either这样做:

def main(args: Array[String]) {
  val f = {
    if (args.length < 1) Left("No filename given")
    else {
      val file = new File(args(0))
      if (!file.exists) Left("File does not exist: "+args(0))
      else Right(file)
    }
  }
  // ...
}

现在假设您要解析一个以空格分隔的数字的字符串。

val numbers = "1 2 3 fish 5 6"      // Uh-oh
// numbers.split(" ").map(_.toInt)  <- will throw exception!
val tried = numbers.split(" ").map(s => Try(s.toInt))  // Caught it!
val good = tried.collect{ case Success(n) => n }

因此,您有(至少)三种方法来处理不同类型的故障:Option因为它有效/无效,在预期行为不有效的情况下,不是令人震惊和令人担忧的故障;Either当事情可以工作或不工作时(或者,实际上,任何你有两个互斥选项的情况)并且你想保存一些关于哪里出了问题的信息;并且Try当您不想自己处理异常的整个头痛,但仍需要与异常快乐的代码交互时。

顺便说一句,例外是很好的例子——所以你会在教科书或学习材料中发现它们比在其他地方更常见,我认为:教科书的例子通常是不完整的,这意味着通常可以通过精心设计来避免的严重问题应该而是通过抛出异常来标记。

*编辑:段错误使 JVM 崩溃,无论字节码如何,都不应该发生;那时即使是例外也无济于事。我的意思是堆栈溢出。

**编辑:异常(没有堆栈跟踪)也用于 Scala 中的控制流——它们实际上是一种非常有效的机制,它们可以启用库定义的break语句和return从您的方法返回的东西,即使控制实际上已经进入一个或多个闭包。大多数情况下,您自己不应该担心这一点,只是要意识到捕获所有 Throwables 并不是一个好主意,因为您可能会错误地捕获这些控制流异常之一。

于 2012-10-14T21:00:04.700 回答
12

因此,这是 Scala 专门权衡功能纯度以换取与遗留语言和环境(特别是 Java)的易于转换/互操作性的地方之一。异常会破坏功能纯度,因为它们破坏了参照完整性,并且无法进行等式推理。(当然,非终止递归也是如此,但很少有语言愿意强制执行那些不可能实现的限制。)为了保持功能的纯洁性,您使用 Option/Maybe/Either/Try/Validation,所有这些都编码成功或失败作为引用透明类型,并使用它们提供的各种高阶函数或底层语言特殊的 monad 语法使事情更清晰。或者,在 Scala 中,您可以简单地决定放弃功能纯度,知道这可能会使事情在短期内变得更容易,但在长期内会变得更加困难。这类似于在 Scala 中使用“null”、可变集合或本地“var”。有点可耻,不要做太多,但每个人都在最后期限内。

于 2012-10-14T22:36:40.663 回答