我已经思考了一段时间,发现自己不断发现警告和矛盾,所以我希望有人可以得出以下结论:
对于错误代码的偏爱我知道,从从事该行业四年开始,阅读书籍和博客等以来,当前处理错误的最佳实践是抛出异常,而不是返回错误代码(不一定返回错误代码,而是一种表示错误代码的类型)。错误)。但是-对我来说,这似乎是矛盾的...
对接口而不是对实现进行编码
我们对接口或抽象进行编码以减少耦合。我们不知道或不想知道接口的特定类型和实现。那么,我们怎么可能知道应该寻找哪些异常呢?该实现可以抛出10个不同的异常,也可以不抛出任何异常。确实,当我们捕获到异常时,我们正在对实现进行假设吗?
-除非接口具有...
异常规范
,某些语言允许开发人员声明某些方法抛出某些异常(例如,Java使用throws关键字。)从调用代码的角度来看,这似乎很好-我们明确知道可能需要捕获哪些异常。
但是-这似乎暗示了...
抽象泄漏
为什么接口应该指定可以抛出哪些异常?如果实现不需要引发异常或需要引发其他异常怎么办?在接口级别,无法知道实现可能要抛出的异常。
所以...
总结
当异常(在我看来)与软件最佳实践相矛盾时,为什么还要优先考虑它们?而且,如果错误代码非常糟糕(并且我不需要在错误代码的恶习上卖掉),还有其他选择吗?满足上述最佳实践要求但不依赖于调用代码检查错误代码返回值的错误处理技术的当前(或即将成为最新技术)是什么?

评论

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

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

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

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

#1 楼

首先,我不同意以下声明:


优先于错误代码的异常


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

但是它是真的许多接口都因抛出异常而泄漏了其抽象。我相信这不是错误传播/处理的“异常”样式的错误。总的来说,我认为有关错误处理的最佳建议是:

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

我认为如果有人遵守该规则根据经验,抽象的“泄漏”量可能非常有限并且可以控制。

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

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

这全都归结为一件事:
好的接口设计。 >如果您对接口的设计足够好,那么就不会有太多的异常困扰。
否则,不仅仅是异常会困扰您。

此外,我认为Java的创建者具有强烈的安全原因,包括方法声明/定义的异常。

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

评论


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

–尼尔
2012年5月3日12:59



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

– DeadMG
2012年5月3日16:39

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

–安迪
2012年5月4日在1:56

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

–迈克尔·伯格沃德(Michael Borgwardt)
2012年5月4日下午6:56

@Giorgio:请参阅artima.com/intv/handcuffs.html-特别是第2和第3页。

–迈克尔·伯格沃德(Michael Borgwardt)
2012年5月7日在9:34

#2 楼

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

您可以像这样使用一种方法由Haskell拍摄,其中错误可以通过具有多个构造函数的抽象数据类型来表示(认为有区别的枚举或null指针,但类型安全,并且可以添加语法糖或辅助函数以使代码流看起来不错)。 >
func x = do
    a <- operationThatMightFail 10
    b <- operationThatMightFail 20
    c <- operationThatMightFail 30
    return (a + b + c)


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

另一种可能性是将错误处理程序对象作为额外的参数传递给您调用的每个函数。该错误处理程序为每个可能的“异常”提供了一种方法,该方法可以通过传递给它的函数来发出信号,并且可以由该函数用于处理发生异常的异常,而不必通过异常回退堆栈。 br />
通用LISP可以做到这一点,并通过语法支持(隐式参数)和内置函数遵循此协议使其变得可行。

评论


真的很整洁。感谢您回答有关替代品的部分:)

– RichK
2012年5月3日在16:34

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

– 9000
2012年5月3日17:28

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

–乔治
13年6月13日在17:33

#3 楼

是的,异常可能导致抽象泄漏。但是,错误代码在这方面是否还更糟?通过在必要时捕获,转换和重新抛出异常来对此模型进行建模。如果您需要“完美”的接口,那是应该走的路。

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

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

评论


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

–乔治
2012年5月4日21:52

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

–迈克尔·伯格沃德(Michael Borgwardt)
2012年5月4日在22:04

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

–乔治
2012年5月4日在22:31

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

–sleske
2012年5月7日上午8:13

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

–sleske
2012年5月7日晚上8:15

#4 楼

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

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

我见过的另一种简陋的异常用法案例是,那些人的第一反应是是“引发例外”。这几乎总是在不编写catch的情况下完成的(经验法则:先编写catch,然后再抛出throw语句)。在大型应用程序中,当未捕获的异常从下层区域冒出来并炸毁程序时,这将成为问题。

我不是反异常,但它们看起来像几年前的单例:过于频繁和不当使用。它们非常适合预期的用途,但是这种情况并不像某些人想象的那么广泛。

#5 楼


抽象泄漏

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


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

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

评论


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

–hugomg
2012年5月3日16:55



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

– DeadMG
2012年5月3日在16:59

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

–乔治
2012年5月7日,9:11

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

–hugomg
2012年5月7日15:01



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

–乔治
2012年5月7日17:36

#6 楼

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

解决设计问题的方法是有两个接口/抽象实现。一个用于功能,另一个用于异常处理。并根据捕获的Exception的类型,调用适当的异常类型类。错误代码的实现是处理异常的一种常规方法。这就像字符串与字符串生成器的用法一样。

评论


换句话说:实现会抛出api中定义的异常的子类。

– andrew cooke
2012年5月2日,12:47

#7 楼

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

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

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


太抽象而无法处理即使异常本身被向上传播,该异常也会以任何有意义的方式;
正在执行通用功能(不必在乎您为什么失败,例如消息泵或可观察的对象);
是特定的,但职责不同,真的不要担心;

考虑到上述情况,用throws语义修饰接口只是一种边际功能收益,因为通过接口协定,许多调用者会只关心您是否失败,而不是为什么。

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

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

#8 楼


优先于错误代码的异常



两者应共存。
当您预期某些行为时,请返回错误代码。
如果您没有预期的行为,请返回异常。
在保留异常类型的情况下,错误代码通常与单个消息相关联,但是消息可能有所不同
在错误代码没有异常的情况下,异常具有堆栈跟踪。我不使用错误代码来调试损坏的系统。


编码接口而不是实现


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


当我们肯定捕获到异常时,有关
实现的假设?


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


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


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

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

#9 楼


抽象泄漏

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


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

当然,在某些情况下需要为错误附加特定的语义,并根据发生的错误做出不同的反应。这种情况应在接口规范中记录。但是,该接口可能仍然保留引发其他异常的权利,而没有特殊含义。

#10 楼

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

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

总结:



我认为异常可以使代码更简洁,并且可以更积极地测试和调试,因为未捕获的异常非常多另一方面,错误代码比错误代码更容易看到和忽略。
另一方面,在测试过程中未发现的未捕获的异常错误会以崩溃的形式出现在生产环境中。在某些情况下,这种行为是不可接受的,在这种情况下,我认为使用错误代码是一种更可靠的方法。


评论


我不同意。是的,未捕获的异常可能会使应用程序崩溃,但未经检查的错误代码也可能使应用程序崩溃-因此这很容易。如果您正确地使用了异常,那么唯一的未捕获异常将是致命异常(例如OutOfMemory),对于这些异常,立即崩溃是您可以采取的最佳措施。

–sleske
2012年5月7日晚上8:19

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

–乔治
2012年5月7日8:25



第一:您可以检查所有异常-只需执行“口袋妖怪异常处理”(必须捕获所有异常-即捕获Exception甚至Throwable或等效的异常)。

–sleske
2012年5月7日在8:46

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

–sleske
2012年5月7日在8:48

#11 楼

要么右偏。

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

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

评论


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

– gna
2014年12月22日13:53

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

– Keynan
2014-12-23 23:05