使用异常作为工具尽早“捕获”错误是否可以?


29

我使用异常来尽早发现问题。例如:

public int getAverageAge(Person p1, Person p2){
    if(p1 == null || p2 == null)
        throw new IllegalArgumentException("One or more of input persons is null").
    return (p1.getAge() + p2.getAge()) / 2;
}

我的程序永远不要传递null此函数。我从来没有打算。但是,众所周知,编程中会发生意想不到的事情。

如果发生此问题,将引发异常,以便在导致程序其他位置出现更多问题之前,找出并修复该异常。异常会停止程序,并告诉我“这里发生了坏事,请修复它”。而不是null在程序周围四处走动,从而导致其他地方出现问题。

现在,您是对的,在这种情况下,这null将立即导致NullPointerException立马,所以它可能不是最佳示例。

但以这种方法为例:

public void registerPerson(Person person){
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

在这种情况下,null参数as将在程序周围传递,并且可能在以后导致错误,这将很难追溯到其来源。

像这样更改功能:

public void registerPerson(Person person){
    if(person == null) throw new IllegalArgumentException("Input person is null.");
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

使我能够在其他地方引起奇怪错误之前发现问题。

另外,null作为参数的引用仅是示例。从无效的参数到其他任何问题,可能有很多问题。最好早点发现它们。


所以我的问题很简单:这是好习惯吗?我将异常用作预防问题的工具是否很好?这是合法的例外适用还是有问题?


2
下降投票者能否解释这个问题出了什么问题?
Giorgio

2
“早期崩溃,经常崩溃”
Bryan Chen

16
从字面上看,这就是例外。
raptortech97

请注意,编码协定使您能够静态检测许多此类错误。通常,这比在运行时引发异常更好。编码合同的支持和有效性因语言而异。
布莱恩

3
由于您将引发IllegalArgumentException,因此说“ 输入人” 是多余的。
凯文·克莱恩

Answers:


29

是的,“尽早失败”是一个很好的原则,这只是实现它的一种可能方式。在必须返回特定值的方法中,有很多事情可以使您故意失败-它要么抛出异常,要么触发断言。异常应该表示“异常”情况,并且检测编程错误当然是例外。


如果无法从错误或状况中恢复,则应尽早失败。例如,如果您需要复制文件并且源或目标路径为空,则应立即引发异常。
Michael Shopsin 2014年

它也被称为“快速失败”
keuleJ 2014年

8

是的,抛出异常是一个好主意。早扔,经常扔,热切扔。

我知道有一个“例外与断言”辩论,其中有一些异常行为(特别是那些认为反映编程错误的异常行为)由断言处理,这些断言可以“编译”用于运行时,而不是调试/测试构建。但是,在现代硬件上,进行几次额外的正确性检查所消耗的性能是最小的,并且获得正确,正确的结果的价值远远超过了任何额外的成本。我从未真正遇到过我希望在运行时删除(大多数)检查的应用程序代码库。

我很想说我不想在数字密集型代码的紧密循环中进行很多额外的检查和条件操作……但是实际上这是生成大量数字错误的地方,并且如果在那里未被捕获,将向外传播到影响所有结果。因此,即使在那里,检查还是值得做的。实际上,一些最佳,最有效的数值算法是基于错误评估的。

最后一个非常注意额外代码的地方是对延迟很敏感的代码,其中额外的条件会导致管道停顿。因此,在操作系统,DBMS和其他中间件内核以及低级通信/协议处理中间。但是同样,这些是最有可能观察到错误的地方,并且其(安全性,正确性和数据完整性)影响最大。

我发现的一项改进是不只抛出基本级别的异常。IllegalArgumentException是很好的,但它基本上可以来自任何地方。在大多数语言中,添加自定义例外并不需要太多。对于您的人员处理模块,请说:

public class PersonArgumentException extends IllegalArgumentException {
    public MyException(String message) {
        super(message);
    }
}

然后当有人看到 PersonArgumentException,很清楚它的来源。关于要添加的自定义异常的数量,这是一种平衡行为,因为您不想不必要地增加实体的数量(Occam的Razor)。通常,仅几个自定义异常就足以表明“此模块未获取正确的数据!” 或“此模块无法执行应做的事情!” 以一种特定且量身定制的方式,但又不是太精确,因此您必须重新实现整个异常层次结构。我通常从少量的自定义异常开始,首先从股票异常开始,然后扫描代码并意识到“这N个地方正在引发股票异常,但它们归结为更高层次的想法,即他们没有获取数据他们需要;让我们


I've never actually met an application codebase for which I'd want (most) checks removed at runtime. 然后,您还没有完成对性能至关重要的代码。我现在正在做的事情是每秒处理37M个操作,其中包含on的断言,不包含它们的42M。断言不是在那里验证外部输入,而是在那里确保代码正确。一旦我对我的东西没有损坏感到满意,我的客户就很乐意接受13%的增长。
Blrfl 2014年

应该避免在代码库中添加自定义异常,因为虽然这可能会对您有所帮助,但对其他必须熟悉它们的人没有帮助。通常,如果您发现自己扩展了一个通用异常并且不添加任何成员或功能,则实际上并不需要它。即使在您的示例中,PersonArgumentException也不清楚IllegalArgumentException。众所周知,后者会在传递非法论点时抛出。我实际上希望如果a Person处于无效的通话状态(类似于InvalidOperationExceptionC#),则会抛出前者。
Selali Adob​​or 2014年

的确,如果您不小心,可以从函数的其他地方触发相同的异常,但这是代码的缺陷,而不是常见异常的性质。这就是调试和堆栈跟踪的地方。如果您要“标记”异常,请使用大多数常见异常提供的现有功能,例如使用消息构造函数(正是它的用途)。某些例外甚至为构造函数提供了其他参数来获取有问题的参数的名称,但是您可以为同一工具提供格式正确的消息。
Selali Adob​​or 2014年

我承认,仅仅将一个常见的例外重命名为一个基本相同的例外的自有品牌是一个微不足道的例子,很少值得付出努力。在实践中,我尝试以更高的语义级别发出自定义异常,这些异常更紧密地与模块的意图联系在一起。
乔纳森·尤尼斯

我已经在数值/ HPC和OS /中间件领域完成了对性能敏感的工作。13%的性能提升并非易事。我可能会关闭一些检查来获取它,就像军事指挥官可能要求的是反应堆名义输出的105%一样。但是我经常看到“它会以这种方式运行得更快”,这通常是关闭检查,备份和其他保护措施的原因。它基本上是在弹性和安全性(很多情况下,包括数据完整性)之间进行权衡以获得更高的性能。这是否值得,是一个判断电话。
乔纳森·尤尼斯

3

调试应用程序时,尽快失败是非常好的。我记得一个遗留C ++程序中的一个特殊的分段错误:检测到该错误的位置与引入该错误的位置无关(在最终导致问题之前,空指针从内存中的一个位置愉快地移到了另一个位置) )。在这些情况下,堆栈跟踪无法帮助您。

因此,防御编程是一种快速检测和修复错误的有效方法。另一方面,它可能会被夸大,尤其是对于空引用。

在您的特定情况下,例如:如果任何引用为null,则NullReferenceException当试图获得一个人的年龄时,该引用将在下一条语句中抛出。您实际上不需要在这里亲自检查:让底层系统捕获这些错误并引发异常,这就是它们存在的原因

对于更实际的示例,可以使用以下assert语句:

  1. 读写时间较短:

        assert p1 : "p1 is null";
        assert p2 : "p2 is null";
    
  2. 专为您的方法而设计。在既有断言又有异常的世界中,可以按以下方式区分它们:

    • 断言是针对编程错误的(“ / *这永远不会发生* /”),
    • 特殊情况适用于例外情况(例外但可能的情况)

因此,使用断言公开您对应用程序的输入和/或状态的假设,使下一个开发人员可以进一步了解代码的用途。

静态分析器(例如编译器)可能也会更快乐。

最后,可以使用单个开关从已部署的应用程序中删除断言。但总的来说,不要期望以此来提高效率:运行时的断言检查可以忽略不计。


1

据我了解,不同的程序员更喜欢一种解决方案。

通常首选第一个解决方案,因为它更简洁,尤其是您不必在不同的功能中一次又一次地检查相同的条件。

我找到第二个解决方案,例如

public void registerPerson(Person person){
    if(person == null) throw new IllegalArgumentException("Input person is null.");
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

更牢固,因为

  1. 它会尽快捕获错误,即在registerPerson()调用时,而不是在调用堆栈下的某个地方抛出空指针异常时捕获错误。调试变得更加容易:我们都知道无效值可以在代码表现为错误之前经过代码多远。
  2. 它减少了函数之间的耦合:registerPerson()不对使用该person参数最终将使用哪个其他函数以及它们将如何使用该参数进行任何假设:null作为错误的决定是在本地进行并实现的。

因此,尤其是在代码非常复杂的情况下,我倾向于使用第二种方法。


1
给出原因(“输入人为空”。)还有助于理解问题,这与库中某些晦涩的方法失败时不同。
Florian F

1

通常,是的,“尽早失败”是个好主意。但是,在您的特定示例中,显式IllegalArgumentException没有提供对NullReferenceException- 的显着改进,因为操作的两个对象都已作为参数传递给函数。

但是让我们看一个稍微不同的例子。

class PersonCalculator {
    PersonCalculator(Person p) {
        if (p == null) throw new ArgumentNullException("p");
        _p = p;
    }

    void Calculate() {
        // Do something to p
    }
}

如果构造函数中没有检查参数,则NullReferenceException在调用时会得到a Calculate

但是,断掉的代码既不是Calculate函数,也不是函数的使用者Calculate。断断续续的代码是尝试PersonCalculator使用null 构造的代码Person-因此我们希望在其中发生异常。

如果删除该显式参数检查,则必须找出调用NullReferenceException时为什么会发生Calculate。跟踪为什么要由null人构造对象会变得很棘手,尤其是在构造计算器的代码与实际调用该Calculate函数的代码不接近的情况下。


0

不在您提供的示例中。

就像您说的那样,在此后不久您将要获得异常时,明确地抛出并不会给您带来太大的好处。许多人会争辩说,带有明确的例外和好的信息会更好,尽管我不同意。在预发布的方案中,堆栈跟踪足够好。在发布后的场景中,呼叫站点通常可以提供比函数内部更好的消息。

第二种形式是为函数提供过多信息。该函数不必一定知道其他函数将抛出空输入。即使他们扔在空的输入现在,它变得非常麻烦重构应该就是不再是因为空校验的情况是整个代码传播。

但是总的来说,一旦确定出了问题(应该尊重DRY),就应该提早抛出。这些也许不是很好的例子。


-3

在您的示例函数中,我希望您不进行任何检查,仅允许NullReferenceException发生这种情况。

对于其中一个,无论如何在其中传递null毫无意义,因此我将基于抛出来立即找出问题NullReferenceException

第二,如果每个函数根据提供的明显错误的输入类型抛出了略有不同的异常,那么很快您就会拥有可能抛出18种不同类型异常的函数,并且很快您发现自己也说过需要处理很多工作,反正只是抑制所有异常。

您真的无能为力,无法解决函数中设计时错误的情况,因此只需让故障发生而无需更改即可。


我在基于此原则的代码基础上工作了几年:指针在需要时仅被取消引用,几乎从不检查NULL并进行显式处理。调试该代码一直非常耗时且麻烦,因为异常(或核心转储)发生的时间要晚得多,这是逻辑错误所在的时间。我浪费了数周的时间来寻找某些错误。由于这些原因,我更喜欢“尽一切可能失败”的风格,至少对于复杂的代码而言,在这种情况下,空指针异常的来源并不太明显。
Giorgio 2014年

“对于一个,无论如何在其中传递null毫无意义,所以我将基于抛出NullReferenceException来立即解决问题。”:不一定,如Prog的第二个示例所示。
Giorgio 2014年

“第二,如果每个函数根据提供的错误输入类型明显抛出不同的异常,那么很快您就会拥有可能抛出18种不同类型的异常的函数……”:在这种情况下,您可以在所有函数中使用相同的异常(甚至NullPointerException):重要的是尽早抛出,而不是从每个函数抛出不同的异常。
Giorgio 2014年

那就是RuntimeExceptions的目的。任何“不应发生”但阻止您的代码正常工作的条件都应抛出一个条件。您无需在方法签名中声明它们。但是您需要在某个时候捕获它们,并报告请求的操作功能失败。
Florian F

1
该问题未指定语言,因此依赖于例如RuntimeException不一定是有效的假设。
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.