执行错误处理的现代方法…


118

我已经思考了一段时间了,发现自己不断发现警告和矛盾,因此我希望有人可以得出以下结论:

优先于错误代码的异常

据我所知,在从事该行业工作四年,阅读书籍和博客等之后,当前处理错误的最佳实践是抛出异常,而不是返回错误代码(不一定返回错误代码,而是返回错误代码)。类型代表错误)。

但是-对我来说这似乎是矛盾的...

编码接口,而不是实现

我们对接口或抽象进行编码以减少耦合。我们不知道或不想知道接口的特定类型和实现。那么,我们怎么可能知道应该寻找哪些异常呢?该实现可以抛出10个不同的异常,也可以不抛出任何异常。当我们确实捕获到异常时,我们正在对实现进行假设?

除非-接口具有...

异常规格

某些语言允许开发人员声明某些方法会引发某些异常(例如,Java使用throws关键字。)从调用代码的角度来看,这似乎很好-我们明确知道可能需要捕获哪些异常。

但是-这似乎表明...

泄漏抽象

为什么接口应该指定可以引发哪些异常?如果实现不需要引发异常或引发其他异常怎么办?在接口级别,无法知道实现可能要抛出的异常。

所以...

总结一下

当异常(在我看来)与软件最佳实践相矛盾时,为什么还要优先考虑它们?而且,如果错误代码非常糟糕(并且我不需要在错误代码的恶习上卖掉),还有其他选择吗?满足(如上所述)最佳实践要求但不依赖于调用代码检查错误代码返回值的错误处理技术的当前(或即将成为最新技术)是什么?


12
我不了解您对泄漏抽象的观点。指定特定方法可能引发的异常是接口规范的一部分。就像返回值的类型是接口规范的一部分一样。异常只是不会“移出”函数的左侧,但它们仍然是接口的一部分。
deceze

@deceze接口如何说明实现可能抛出的内容?它可能会在类型系统中引发所有异常!而且许多语言不支持异常规范建议他们非常狡猾
RichK

1
我同意,如果方法本身使用其他方法并且在内部不捕获其异常,则管理该方法可能引发的所有不同异常可能很棘手。话虽如此,这不是矛盾。
deceze

1
这里的泄漏假设是,代码在脱离接口边界时可以处理异常。考虑到它对实际出了什么问题的了解不多,这几乎是不可能的。它所知道的只是出了点问题,并且接口实现不知道如何处理。除了报告并终止程序之外,它可以做得更好的可能性很小。如果接口对于正确的程序操作不是至关重要的,则可以忽略它。您不能也不应在接口协定中编码的实现细节。
汉斯·帕桑

Answers:


31

首先,我不同意这一说法:

优先于错误代码的异常

并非总是如此:例如,看一下Objective-C(使用Foundation框架)。尽管存在Java开发人员称为真正异常的东西,但NSError是处理错误的首选方式:@ try,@ catch,@ throw,NSException类等。

但是,许多接口确实会泄漏其抽象,并抛出异常。我相信这不是错误传播/处理的“异常”样式的错误。总的来说,我相信有关错误处理的最佳建议是:

在尽可能低的级别,周期内处理错误/异常

我认为如果坚持这一经验法则,抽象的“泄漏”量可能会非常有限并受到控制。

关于方法抛出的异常是否应该作为其声明的一部分,我认为它们应该:它们是此接口定义的协定的一部分:该方法执行A,或者对于B或C失败。

例如,如果一个类是XML解析器,则其设计的一部分应该表明所提供的XML文件完全是错误的。在Java中,通常通过声明预期遇到的异常并将其添加到throws方法的声明部分中来实现。另一方面,如果其中一种解析算法失败,则没有理由将该异常传递给未处理的对象。

归结为一件事: 良好的界面设计。 如果您对界面的设计足够好,那么就不会有任何异常困扰。否则,打扰您的不仅是例外。

另外,我认为Java的创建者出于安全方面的考虑,非常强烈地将方法声明/定义的异常包括在内。

最后一件事:某些语言,例如Eiffel,具有其他错误处理机制,并且根本不包含抛出功能。当不满足例程的后置条件时,将自动引发排序的“异常”。


12
+1表示否定“对错误代码的偏爱”。有人告诉我,例外是好事,只要它们实际上是例外而不是规则。调用引发异常的方法来确定条件是否为真是非常糟糕的做法。
尼尔

4
@JoshuaDrake:他肯定是错的。例外和goto非常不同。例如,异常总是沿着相同的方向-向下到达调用堆栈。其次,保护自己免受意外异常的侵害与DRY完全相同,例如,在C ++中,如果使用RAII来确保清除,那么它可以确保在所有情况下都进行清除,不仅是例外,而且还包括所有常规控制流。这是绝对可靠的。try/finally完成一些类似的事情。在适当保证清理的情况下,您无需将异常视为特殊情况。
DeadMG

4
@JoshuaDrake抱歉,Joel离那里很远。异常与goto不同,您总是将调用堆栈至少上一层。如果上述级别不能立即处理错误代码,您该怎么办?返回另一个错误代码?错误的部分原因是可以忽略它们,从而导致比抛出异常更严重的问题。
安迪

25
-1是因为我完全不同意“以尽可能最低的级别处理错误/异常”-这是错误的,句号。在适当的级别处理错误/异常。通常这是一个更高的级别,在这种情况下,错误代码是一个巨大的痛苦。支持使用异常而不使用错误代码的原因是,它们使您可以自由选择在哪个级别上进行处理,而不会影响两者之间的级别。
Michael Borgwardt'5

2
@Giorgio:看artima.com/intv/handcuffs.html -尤其是第2和3
迈克尔博格瓦特

27

我只想指出,异常和错误代码不是处理错误和备用代码路径的唯一方法。

您可以采用Haskell所采取的方法,在这种方法中,可以通过具有多个构造函数的抽象数据类型来发出错误信号(请考虑区别枚举或空指针,但类型安全,并且可以添加句法)糖或辅助函数,以使代码流看起来不错)。

func x = do
    a <- operationThatMightFail 10
    b <- operationThatMightFail 20
    c <- operationThatMightFail 30
    return (a + b + c)

operationThatMightfail是一个函数,返回包装在Maybe中的值。它的工作方式类似于可为空的指针,但是do表示法可确保如果a,b或c中的任何一个失败,则整个对象的计算结果为null。(并且编译器可以防止您意外产生NullPointerException)

另一种可能性是将错误处理程序对象作为附加参数传递给您调用的每个函数。该错误处理程序为每种可能的“异常”提供了一种方法,该方法可以通过传递给它的函数来发出信号,并且该函数可以使用该方法来处理发生异常的异常,而不必通过异常回退堆栈。

通用的LISP可以做到这一点,并通过语法支持(隐式参数)并使内置函数遵循此协议,从而使其可行。


1
真的很整洁。感谢您回答有关替代方案的部分:)
RichK,2012年

1
顺便说一句,例外是现代命令式语言中功能最强大的部分之一。异常构成一个单子。异常使用模式匹配。异常有助于模仿应用样式,而无需实际学习内容Maybe
9000年

我最近正在读一本关于SML的书。它提到了选项类型,异常(和延续)。建议是在预计发生未定义的情况时会经常使用选项类型,而在未定义的情况很少发生时使用异常。
Giorgio 2013年

8

是的,异常会导致抽象泄漏。但是错误代码在这方面是否还更糟?

解决此问题的一种方法是让接口准确指定在什么情况下可以抛出哪些异常,并声明实现必须将其内部异常模型映射到此规范,并在必要时捕获,转换和重新抛出异常。如果您想要一个“完美”的界面,那就是这样。

在实践中,通常足以指定逻辑上属于接口的异常,并且客户端可能希望捕获并对其进行处理。通常可以理解,当发生低级错误或出现错误时,可能还会有其他异常,并且客户端只能通过显示错误消息和/或关闭应用程序来进行一般处理。至少异常仍然可以包含有助于诊断问题的信息。

实际上,使用错误代码,几乎所有相同的事情最终都会发生,只是以更加隐式的方式发生,并且信息丢失的可能性大大增加,并且应用程序最终处于不一致状态。


1
我不明白为什么错误代码会导致抽象泄漏。如果返回的是错误代码而不是正常的返回值,并且此行为在函数/方法的规范中进行了描述,则IMO没有泄漏。还是我忽略了什么?
乔治

如果错误代码是特定于实现的,而API应该与实现无关,则指定并返回错误代码会泄漏不必要的实现细节。典型示例:具有基于文件和基于数据库的实现的日志记录API,其中前者可能会出现“磁盘已满”错误,而后者可能会出现“主机拒绝数据库连接”错误。
Michael Borgwardt 2012年

我明白你的意思。如果您要报告特定于实现的错误,则该API不能与实现无关(既不包含错误代码也不包含异常)。我猜唯一的解决方案是定义一个与实现无关的错误代码,例如“ resource not available”,或者确定该API与实现无关。
乔治

1
@Giorgio:是的,使用与实现无关的API来报告特定于实现的错误非常棘手。不过,您可以使用例外进行处理,因为(与错误代码不同)它们可以具有多个字段。因此,您可以使用异常的类型来提供常规错误信息(ResourceMissingException),并在字段中包含特定于实现的错误代码/消息。两全其美 :-)。
sleske,2012年

顺便说一句,这就是java.lang.SQLException所做的。它具有getSQLState(通用)和getErrorCode(特定于供应商)。现在,如果它只有适当的子类……
sleske,2012年

5

在这里有很多好东西,我想补充一点,我们都应该警惕使用异常作为常规控制流程一部分的代码。有时人们陷入陷阱,凡是不是通常情况的事情都变成例外。我什至还看到过一个异常被用作循环终止条件。

异常表示“我在这里无法处理的事情发生了,需要去找其他人弄清楚该怎么做”。用户键入无效的输入也不例外(输入应通过再次询问等在本地进行处理)。

我见过的另一种退化的异常用法案例是,人们的第一反应是“抛出异常”。几乎总是在不编写catch的情况下完成此操作(经验法则:先编写catch,然后编写throw语句)。在大型应用程序中,当未捕获的异常从下层区域冒出来并使程序崩溃时,这将成为问题。

我不是反例外,但它们似乎像几年前的单例:使用得太频繁且不合适。它们非常适合预期的用途,但是这种情况并不像某些人想象的那样广泛。


4

泄漏抽象

为什么接口应该指定可以引发哪些异常?如果实现不需要引发异常或引发其他异常怎么办?在接口级别,无法知道实现可能要抛出的异常。

不。异常规范与返回和参数类型位于同一存储桶中,它们是接口的一部分。如果您不符合该规范,则不要实现该接口。如果您从不扔东西,那很好。在接口中指定异常没有任何泄漏。

错误代码非常糟糕。太可怕了 您必须手动记住每次检查并传播每个呼叫。首先,这违反了DRY,并且极大地破坏了您的错误处理代码。这种重复比任何异常都要大得多。您永远不能默默地忽略异常,但是人们可以并且确实默默地忽略返回码,这绝对是一件坏事。


如果您有良好形式的语法糖或辅助方法,则错误代码可能更易于使用,并且在某些语言中,您可以使编译器和类型系统保证您永远不会忘记处理错误代码。至于异常接口部分,我认为他正在考虑Java的检查异常臭名昭著的笨拙。乍一看它们似乎是一个非常合理的想法,但它们在实践中会引起很多痛苦的小问题。
hugomg

@missingno:那是因为按照惯例,Java具有可怕的实现,而不是因为检查异常本质上是不好的。
DeadMG

1
@missingno:受检查的异常可能导致什么类型的问题?
乔治

@Giorgio:很难预见方法可能引发的每种异常,因为这需要考虑其他尚未编写的子类和代码。在实践中,人们最终会遇到丢掉信息的丑陋变通办法,例如对所有内容重用系统异常类,或者不得不频繁地捕获和重新抛出内部异常。我还听说,当试图向语言添加匿名函数时,检查异常是一个很大的障碍。
hugomg 2012年

@missingno:Java中的AFAIK匿名函数仅是使用一种方法的匿名内部类的语法糖,所以我不确定我为什么理解检查异常会是一个问题(但是我承认我对该主题不了解很多)。是的,很难预见方法会抛出什么异常,这就是IMO为什么检查异常对您有用的原因,因此您不必猜测。当然,您可以编写可怜的代码来处理它们,但是您也可以使用未经检查的异常来处理。但是,我知道辩论是非常复杂的,老实说,我看到双方都有利弊。
乔治

2

那么异常处理可以有自己的接口实现。根据引发的异常类型,执行所需的步骤。

设计问题的解决方案是具有两个接口/抽象实现。一个用于功能,另一个用于异常处理。并根据捕获的异常类型,调用适当的异常类型类。

错误代码的实现是处理异常的一种常规方法。这就像字符串与字符串生成器的用法一样。


4
换句话说:实现会抛出api中定义的异常的子类。
安德鲁·库克

2

IM-ver-HO异常将根据具体情况进行判断,因为通过中断控制流,它们将增加代码的实际和可感知的复杂性,在许多情况下则不必要。抛弃与在函数内部引发异常有关的讨论-这实际上可能会改善您的控制流程,如果要研究通过调用边界引发异常,请考虑以下事项:

允许被呼叫者中断您的控制流可能不会提供任何实际好处,并且可能没有有意义的方式来处理异常。举一个直接的例子,如果正在实现Observable模式(使用C#之类的语言,您到处都有事件,并且throws在定义中没有明确的提示),则没有实际的理由让Observer崩溃时破坏您的控制流,并且没有有意义的方式来处理他们的东西(当然,观察时一个好邻居不应该扔,但是没有人是完美的)。

上面的观察可以扩展到任何松散耦合的接口(如您所指出的)。我认为,在爬升3-6个堆栈帧之后,未捕获的异常很可能会出现在一段代码中,这实际上是一种规范:

  • 太抽象了,以至于无法以任何有意义的方式处理该异常,即使该异常本身是向上转换的;
  • 正在执行一般功能(根本不在乎失败的原因,例如消息泵或可观察的对象);
  • 是特定的,但职责不同,确实不应该担心;

考虑到上述情况,用throws语义修饰接口仅是微不足道的功能增益,因为通过接口协定的许多调用者只会在您失败时关心,而不是为什么。

我要说的是,这将成为滋味和便利性的问题:您的主要重点是在“异常”之后优雅地恢复呼叫者和被呼叫者的状态,因此,如果您有丰富的错误代码处理经验(即将从C的背景),或者如果您正在异常可能变成邪恶的环境(C ++)中工作,我不认为扔东西对于良好,干净的OOP是如此重要,以至于您不能依靠旧的模式,如果您不满意。特别是如果它导致SoC崩溃。

从理论上讲,我认为处理异常的SoC方式可以直接从以下观察中得出:大多数情况下,直接调用方只关心您失败了,而不是为什么。被调用者抛出,非常接近上方的某人(2-3帧)捕获了一个向上转换的版本,并且实际的异常始终沉入到专门的错误处理程序中(即使仅进行跟踪)-这是AOP派上用场的地方,因为这些处理程序可能是水平的。


1

优先于错误代码的异常

  • 两者应该共存。

  • 当您预期某些行为时,请返回错误代码。

  • 当您没有预期到某些行为时,请返回异常。

  • 当保留异常类型时,错误代码通常与一条消息相关联,但是一条消息可能会有所不同

  • 异常具有堆栈跟踪,而错误代码则没有。我不使用错误代码来调试损坏的系统。

编码接口而不是实现

这可能是特定于JAVA的,但是当我声明接口时,我没有指定该接口的实现可能引发的异常,这只是没有意义。

当我们确实捕获到异常时,我们正在对实现进行假设?

这完全取决于您。您可以尝试捕获非常具体的异常类型,然后捕获更一般的异常Exception。为什么不让异常在堆栈中传播然后处理呢?或者,您可以查看方面编程,其中异常处理成为“可插入”方面。

如果实现不需要引发异常或引发其他异常怎么办?

我不明白为什么这对您来说是个问题。是的,您可能有一个永远不会失败或引发异常的实现,并且您可能有一个不断失败并引发异常的实现。如果是这种情况,请不要在接口上指定任何异常,这样就可以解决您的问题。

如果您的实现返回一个结果对象而不是异常,它将改变任何东西吗?该对象将包含您的操作结果以及任何错误/失败(如果有)。然后,您可以讯问该对象。


1

泄漏抽象

为什么接口应该指定可以引发哪些异常?如果实现不需要引发异常或引发其他异常怎么办?在接口级别,无法知道实现可能要抛出的异常。

以我的经验,接收错误的代码(通过异常,错误代码或其他任何方式)通常不会在意错误的确切原因-它将对任何失败做出相同的反应,但可能会报告错误。错误(错误对话框或某种日志);并且此报告将与调用失败过程的代码正交进行。例如,此代码可以将错误传递给其他一些知道如何报告特定错误(例如,格式化消息字符串)的代码,并可能附加一些上下文信息。

当然,在某些情况下,它需要特殊的语义附加到错误,并作出不同的反应在此基础上出现错误。这种情况应在接口规范中记录。但是,接口可能仍然保留引发其他异常的权利,而没有特殊含义。


1

我发现异常允许编写更加结构化和简洁的代码来报告和处理错误:使用错误代码需要在每次调用后检查返回值,并确定在出现意外结果时该怎么做。

另一方面,我同意例外揭示了实现细节,这些细节应隐藏在调用接口的代码中。由于不可能先验地知道哪段代码会引发哪些异常(除非它们像在Java中那样在方法签名中声明),因此通过使用异常,我们在代码的不同部分之间引入了非常复杂的隐式依赖关系,即反对最小化依赖的原则。

总结:

  • 我认为异常允许使用更干净的代码,并且可以采用更具侵略性的方法进行测试和调试,因为未捕获的异常比错误代码(很快失败)更加明显并且更难以忽略。
  • 另一方面,在测试过程中未发现的未捕获的异常错误会以崩溃的形式出现在生产环境中。在某些情况下,这种行为是不可接受的,在这种情况下,我认为使用错误代码是一种更可靠的方法。

1
我不同意。是的,未捕获的异常可能会使应用程序崩溃,但未经检查的错误代码也可能使应用程序崩溃-因此这很容易。如果正确使用异常,则只有致命的异常(如OutOfMemory)才是未捕获的异常,对于这些异常,立即崩溃是您能做的最好的事情。
sleske'5

错误代码是呼叫者m1和被呼叫者m2之间的合同的一部分:可能的错误代码仅在m2的接口中定义。对于异常(除非您正在使用Java并在方法签名中声明所有抛出的异常),在调用方m1和m2可以递归调用的所有方法之间具有隐式协定。因此,不检查返回的错误代码当然是一个错误,但是始终可以这样做。另一方面,并​​非总是可能检查方法抛出的所有异常,除非您知道如何实现。
乔治(Giorgio)

首先:您可以检查所有异常-只需执行“口袋妖怪异常处理”(必须捕获所有异常-即catch Exception或什至Throwable,或等效项)。
sleske,2012年

1
实际上,如果API的设计正确,它将指定客户端可以有意义地处理的所有异常-需要专门捕获这些异常。这些是错误代码的等效项。任何其他异常都意味着“内部错误”,应用程序将需要关闭,或者至少关闭相应的子系统。您可以根据需要捕获这些内容(请参见上文),但通常应让它们冒出来。“冒泡”是使用异常的主要优点。您仍然可以根据需要将它们捕获得更远或更远。
sleske,2012年

-1

右偏。

它不能忽略,必须加以处理,它是完全透明的。如果使用正确的左手错误类型,则它传达的所有信息都与Java异常相同。

缺点?具有适当错误处理的代码看起来令人作呕(所有机制均适用)。


在先前的10个答案中,这似乎并没有提供任何实质性的解释。为什么用这样的东西碰到两年多的问题
2014年

除非这里没有人提到偏见的权利。hugomg密切讨论了haskell,但是也许它是一个糟糕的错误处理程序,因为它没有说明错误发生的原因,也没有任何直接的恢复方法,并且回叫是控制流设计中最大的罪过之一。这个问题出现在谷歌上。
Keynan 2014年
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.