为什么要使用(检查过的)异常之一?


17

不久之前,我开始使用Scala而不是Java。对我来说,在这些语言之间进行“转换”过程的一部分是学习使用Eithers而不是(checked)Exceptions。我已经用这种方式编码了一段时间,但是最近我开始怀疑这是否真的是更好的方法。

一个主要的优点Either拥有Exception更好的性能; 一个Exception需要建立一个堆栈跟踪和被抛出。据我了解,虽然Exception不是必须的,但构建堆栈跟踪是必需的。

但随后,人们总是可以构建/继承Exceptions的scala.util.control.NoStackTrace,更是这样,我看到很多的情况下,其中的左侧Either实际上是一个Exception(免收性能提升)。

另一个优点Either是编译器安全。Scala编译器不会抱怨未处理Exception的(不同于Java的编译器)。但是,如果我没记错的话,那么这个决定就是用与本主题中讨论的相同的推理来推理的,所以...

在语法方面,我觉得Exception-style更清晰。检查以下代码块(均实现相同的功能):

Either 样式:

def compute(): Either[String, Int] = {

    val aEither: Either[String, String] = if (someCondition) Right("good") else Left("bad")

    val bEithers: Iterable[Either[String, Int]] = someSeq.map {
        item => if (someCondition(item)) Right(item.toInt) else Left("bad")
    }

    for {
        a <- aEither.right
        bs <- reduce(bEithers).right
        ignore <- validate(bs).right
    } yield compute(a, bs)
}

def reduce[A,B](eithers: Iterable[Either[A,B]]): Either[A, Iterable[B]] = ??? // utility code

def validate(bs: Iterable[Int]): Either[String, Unit] = if (bs.sum > 22) Left("bad") else Right()

def compute(a: String, bs: Iterable[Int]): Int = ???

Exception 样式:

@throws(classOf[ComputationException])
def compute(): Int = {

    val a = if (someCondition) "good" else throw new ComputationException("bad")
    val bs = someSeq.map {
        item => if (someCondition(item)) item.toInt else throw new ComputationException("bad")
    }

    if (bs.sum > 22) throw new ComputationException("bad")

    compute(a, bs)
}

def compute(a: String, bs: Iterable[Int]): Int = ???

后者对我来说看起来要干净得多,在两种情况下,处理故障的代码(模式匹配Eithertry-catch)都很清楚。

所以我的问题是-为什么要使用Eitherover(选中)Exception

更新资料

阅读答案后,我意识到我可能未能展示我困境的核心。我的担心不是没有缺乏try-catch; 可以“捕获”一个Exceptionwith Try,也可以使用catch将该异常包装为Left

我的主要问题是Either/ Try我编写的代码可能会在很多时候失败。在这些情况下,遇到故障时,我必须在整个代码中传播该故障,从而使代码变得更加繁琐(如上述示例所示)。

实际上,还有另一种不Exception使用来破坏代码而不用s的方式return(实际上是Scala中的另一个“ taboo”)。该代码仍比该Either方法更清晰,并且虽然不如该Exception样式简洁,但不必担心会被捕获Exception

def compute(): Either[String, Int] = {

  val a = if (someCondition) "good" else return Left("bad")

  val bs: Iterable[Int] = someSeq.map {
    item => if (someCondition(item)) item.toInt else return Left("bad")
  }

  if (bs.sum > 22) return Left("bad")

  val c = computeC(bs).rightOrReturn(return _)

  Right(computeAll(a, bs, c))
}

def computeC(bs: Iterable[Int]): Either[String, Int] = ???

def computeAll(a: String, bs: Iterable[Int], c: Int): Int = ???

implicit class ConvertEither[L, R](either: Either[L, R]) {

  def rightOrReturn(f: (Left[L, R]) => R): R = either match {
    case Right(r) => r
    case Left(l) => f(Left(l))
  }
}

基本上,return Leftreplaces throw new Exception和任一方法上的隐式方法rightOrReturn是对自动异常在堆栈中向上传播的补充。



@RobertHarvey好像大多数文章都在讨论Try。关于Eithervs 的部分Exception仅声明Either当方法的另一种情况是“非例外”时应使用s。首先,这是一个非常非常模糊的定义恕我直言。其次,这真的值得语法惩罚吗?我的意思是,Either如果不是因为s带来的语法开销,我真的不介意使用s。
Eyal Roth

Either对我来说看起来像单子。当您需要monad提供的功能组合好处时,请使用它。或者,也许不是
罗伯特·哈维

2
@RobertHarvey:从技术上讲,Either它本身不是单子。左侧或右侧的投影是单子,但Either本身不是。您可以通过将其“偏置”到左侧或右侧来使其成为monad。但是,然后您在的两侧赋予了一定的语义Either。Scala Either最初是没有偏见的,但是最近才有偏见,因此,如今,它实际上已经是一元论了,但是“ monadness”不是其固有的特性,Either而是其有偏见的结果。
约尔格W¯¯米塔格

@JörgWMittag:很好。这意味着您可以将其用于其所有的Monadic优点,而不必对结果代码的“干净”程度感到特别烦恼(尽管我怀疑功能样式的延续代码实际上比Exception版本更干净)。
罗伯特·哈维,

Answers:


13

如果您仅使用Either完全一样的命令式try-catch块,那么它将看起来完全像不同的语法来完成完全相同的事情。 Eithers是一个价值。它们没有相同的限制。您可以:

  • 停止保持尝试块较小且连续的习惯。的“捕获”部分Eithers不必紧挨“尝试”部分。它甚至可以在通用功能中共享。这使得正确分离关注点变得容易得多。
  • 存储Eithers在数据结构中。您可以遍历它们并合并错误消息,找到没有失败的第一个消息,懒惰地评估它们或类似内容。
  • 扩展和自定义它们。不喜欢您被迫使用的样板Eithers吗?您可以编写帮助程序功能,以使其更易于在您的特定用例中使用。与try-catch不同,您不会只停留在语言设计者想出的默认界面上。

请注意,对于大多数用例,a Try具有更简单的界面,具有上述大多数优点,并且通常也可以满足您的需求。Option如果您只关心成功或失败,并且不需要任何状态信息(例如有关失败案例的错误消息),则An 更简单。

以我的经验,除非是,Eithers而不是,Trys否则不是真正的首选,也许是验证错误消息,并且您希望汇总这些值。例如,您正在处理一些输入,并且想要收集所有错误消息,而不仅仅是收集第一个错误消息。在实践中,至少在我看来,这种情况很少出现。LeftExceptionLeft

超越try-catch模型,您会发现使用Eithers起来更加愉快。

更新:这个问题仍在引起注意,而且我还没有看到OP的更新澄清语法问题,因此我想我会添加更多内容。

作为突破异常思维方式的一个示例,这就是我通常编写示例代码的方式:

def compute(): Either[String, Int] = {
  Right(someSeq)
    .filterOrElse(someACondition,            "someACondition failed")
    .filterOrElse(_ forall someSeqCondition, "someSeqCondition failed")
    .map(_ map {_.toInt})
    .filterOrElse(_.sum <= 22, "Sum is greater than 22")
    .map(compute("good",_))
}

在这里,您可以看到filterOrElse完美地表达了我们在此功能中主要工作的语义:检查条件并将错误消息与给定故障相关联。因为这是一个非常常见的用例,所以filterOrElse它在标准库中,但是如果不是,则可以创建它。

这是某人提出的反例无法与我完全相同的方式解决的问题,但是我要提出的观点是Eithers,与例外不同,不仅仅是一种使用方法。如果您探索所有可用的功能Eithers并且可以进行试验,则可以采用多种方法来简化大多数错误处理情况的代码。


1.能够将tryand 分开catch是一个很合理的论点,但是用这很难做到Try吗?2. Either可以与该throws机制一起在数据结构中存储;难道不是在处理故障的基础上又增加了一层吗?3.您绝对可以扩展Exceptions。当然,您不能更改try-catch结构,但是处理故障非常普遍,我绝对希望该语言为我提供样板文件(我没有被迫使用)。这就像在说理解是不好的,因为它是单调操作的样板。
Eyal Roth

我刚刚意识到,您说的是Eithers可以扩展,但显然它们不能扩展(都是sealed trait只有两个实现的a final)。您能详细说明一下吗?我假设您的意思不是扩展的字面意思。
Eyal Roth's

1
我的意思是扩展英语单词,而不是编程关键字。通过创建处理常见模式的功能来添加功能。
卡尔·比勒费尔特

8

两者实际上是非常不同的。Either的语义比仅仅表示潜在的失败计算要笼统得多。

Either允许您返回两种不同类型之一。而已。

现在,这两种类型中的一种可能代表错误,也可能不代表错误,而另一种可能代表成功,也可能不代表成功,但这只是许多错误中的一种。Either比这要广泛得多。您也可以使用一种方法,该方法从允许输入名称或日期的用户中读取输入Either[String, Date]

或者,考虑一下C♯中的lambda文字:它们要么评估为an Func或an Expression,即他们评估为匿名函数,要么评估为AST。在C♯中,这“神奇地”发生,但是如果您想给它一个正确的类型,那就是Either<Func<T1, T2, …, R>, Expression<T1, T2, …, R>>。但是,两个选项都不是错误,并且两个选项都不是“正常”或“例外”。它们只是可以从同一函数(或者在这种情况下,归属于相同文字)返回的两种不同类型。[注意:实际上,Func必须分为另外两种情况,因为C♯没有Unit类型,因此不返回任何内容的事物不能与返回某些内容的事物相同地表示,Either<Either<Func<T1, T2, …, R>, Action<T1, T2, …>>, Expression<T1, T2, …, R>>

现在,Either 可以用来表示成功类型和失败类型。而且,如果您同意这两种类型的一致性排序,您甚至可以偏向 Either于偏爱这两种类型中的一种,从而可以通过单链传播故障。但是在那种情况下,您是在赋予某种语义Either,而该语义不一定是它自己拥有的。

Either其两种类型之一固定为错误类型并且偏向一侧以传播错误的An 通常被称为Errormonad,它是表示潜在失败计算的更好选择。在Scala中,这种类型称为Try

与和Try相比,将异常的使用进行比较要有意义得多Either

因此,这就是为什么Either可以在检查的异常上使用它的原因#1 :Either比异常要通用得多。它可以用于甚至没有例外的情况。

但是,您最有可能询问受限情况,即您仅使用Either(或更可能是Try)替代例外。在那种情况下,仍然有一些优势,或者可能有不同的方法。

主要优点是您可以推迟处理错误。您仅存储返回值,甚至无需查看它是成功还是错误。(稍后处理。)或将其传递到其他地方。(让其他人担心。)将它们收集在列表中。(一次处理所有这些。)您可以使用单子链将它们沿处理管道传播。

异常的语义被硬连接到语言中。例如,在Scala中,异常展开堆栈。如果让它们冒泡到异常处理程序,则该处理程序将无法退回发生的位置并对其进行修复。OTOH,如果您在展开并破坏修复它的必要上下文之前将其捕获到堆栈中的较低位置,则可能是因为组件级别太低而无法就如何继续进行明智的决定。

这是在Smalltalk不同,例如,在异常不会展开堆栈,并且是可恢复的,并且可以捕获该异常高高的堆栈(可能高达为调试器),修复了错误waaaaayyyyy 下来的堆栈并从此处继续执行。或CommonLisp条件,其中引发,捕获和处理是三种不同的东西,而不是例外的两种(引发和捕获处理)。

使用实际的错误返回类型代替异常可以使您执行类似的操作,甚至更多,而无需修改语言。

然后房间里有一只明显的大象:例外是一种副作用。副作用是邪恶的。注意:我并不是说这一定是真的。但这是某些人的观点,在这种情况下,错误类型相对于异常的优势显而易见。因此,如果要进行函数式编程,则可能仅由于错误类型是函数式方法而使用错误类型。(请注意,Haskell有例外。也请注意,没有人喜欢使用它们。)


喜欢答案,尽管我仍然不明白为什么要放弃ExceptionEither似乎是一个很棒的工具,但是是的,我是专门询问有关处理故障的问题,与功能强大的工具相比,我宁愿使用更具体的工具来完成任务。推迟失败处理是一个很合理的论点,但是如果他现在不希望处理失败,则可以简单地使用Try抛出an的方法Exception。堆栈跟踪展开也不会影响Either/ Try吗?错误传播错误时,您可能会重复同样的错误;在两种情况下,知道何时处理故障都不是一件容易的事。
Eyal Roth

而且我不能真正与“纯功能”推理争论,不是吗?
Eyal Roth

0

关于异常的好处是,原始代码已经为您分类了特殊情况。an IllegalStateException和a 之间的区别BadParameterException更容易在强类型语言中区分。如果您尝试解析“好”和“坏”,那么您将无法利用可利用的极其强大的工具,即Scala编译器。您自己的扩展名Throwable除了文字消息字符串外,还可以包含所需的信息。

这是TryScala环境中的真正工具,它Throwable可以作为剩余物品的包装,使生活更轻松。


2
我不明白你的意思。
Eyal Roth

Either有样式问题,因为它不是真正的monad(?);left当您将有关特殊情况的信息适当地封装在适当的结构中时,价值的任意性就会避开增加的较高价值。因此,Either在一种情况下,比较左侧使用返回的字符串与try/catch在另一种情况下使用正确键入的字符串进行比较Throwables 是不合适的。由于价值的Throwables提供信息,以及简洁的方式Try把手ThrowablesTry是比任何一个更好的选择...... Eithertry/catch
BobDalgleish

我实际上并不在乎try-catch。就像在问题中所说的那样,后者对我来说看起来要干净得多,并且在两种情况下处理故障的代码(无论是模式匹配还是尝试捕获)都非常清楚。
艾尔·罗斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.