在Scala中抛出异常,啥是“官方规则”

Posted

技术标签:

【中文标题】在Scala中抛出异常,啥是“官方规则”【英文标题】:Throwing exceptions in Scala, what is the "official rule"在Scala中抛出异常,什么是“官方规则” 【发布时间】:2012-10-04 21:01:01 【问题描述】:

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

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

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

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

谁能解释我在这种情况下应该使用什么?

【问题讨论】:

我认为你越偏向于纯函数式 haskell/scalaz 方面,你就会越多地使用 Either/Try。从异常转移到 Either 是一个连续统一体,很难说它们之间的界限在哪里。 好问题,我也对他到处使用异常抛出感到惊讶。但这是真的,Nothing 类型对于多态来说有点优雅。 我问了一个类似的问题:***.com/questions/13012149/…(感谢 om-nom-nom 的链接) 【参考方案1】:

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

让我们举几个例子。

假设您有一个从地图中获取某些内容的方法。会出什么问题?好吧,诸如 segfault* 堆栈溢出之类的戏剧性和危险的事情,或者未找到该元素之类的预期事件。你会让 segfault 堆栈溢出抛出异常,但如果你只是找不到元素,为什么不返回 Option[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,当您不想自己处理异常的整个头痛,但仍需要与异常快乐的代码交互时。

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

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

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

【讨论】:

我要补充一点,Odersky 书中的大部分内容都真正致力于 Scala 语言本身以及从 Java 的过渡,因此缺少一些好的 FP 实践。 很抱歉一年后问这个问题,但您能解释一下您的第二次编辑是什么意思吗?我没听懂你的意思 @SebastienLorber - 如果你有类似def f(xs: Seq[Int]): Int = xs.foreach(i =&gt; if (i&gt;10) return i); 0 的东西,那return 不是真实 return,因为当你在foreach 中时,你' re里面居然有完全不同的方法!为了退出,Scala 实际上用返回值而不是堆栈跟踪抛出了一个异常,并用一个放在f 内容周围的try 块来捕获它。 好的 @RexKerr 谢谢我猜它是 ControlThrowable,也出现在 NonFatal 中 @SebastienLorber - 作为回报,它实际上是scala.runtime.NonLocalReturnControlControlThrowable 的子类。【参考方案2】:

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

【讨论】:

异常如何破坏参照完整性?使用Nothing 作为底部类型,我认为不会有任何东西被“破坏”。如果您的代码纯粹是功能性的,那么异常应该不会造成问题,不是吗?只有在涉及副作用时,它们才会变得棘手。 不,即使没有副作用,惰性也会使异常变得非引用透明。 您能否提供有关 scala 中的参照完整性的链接,我不太明白您所说的破坏功能纯度的意思,谢谢... 我对“异常如何破坏参照完整性”的理解是:我认为 Dave 的意思是“异常破坏参照透明度”。松散地说,如果对于任何给定的输入,总是会产生相同的输出,则函数是引用透明的。例如。如果 x = y,则 someFunction(x) = someOtherFunction(y)。但是异常不是返回值——它们通过添加“除非出现问题”来打破“输入给出输出”的范式。因此,如果您可以将异常编码为另一种形式的返回类型,您仍然可以在函数之间具有引用透明性。 在 Java 中,您必须声明一个方法会引发异常。这使得异常成为公共接口的一部分。此外,如果声明一个方法抛出异常,则必须在 try 块中调用它。 Scala 在这方面有所不同。您不必声明抛出异常。因为在 Java 中你必须声明和处理异常,它们有点成为返回类型的一部分。所以在我看来,这个“引用透明度”问题确实只是 Scala 中的问题。

以上是关于在Scala中抛出异常,啥是“官方规则”的主要内容,如果未能解决你的问题,请参考以下文章

如何在扩展中抛出异常?

在锁 c#2 中抛出异常

为啥在构造函数中抛出异常会导致空引用?

捕获在不同线程中抛出的异常

在java web开发中抛出如下异常,求大神知道!

为啥在 ios/cordova 中抛出这个异常?