反对错误抑制的论点


35

我在我们的一个项目中发现了一段这样的代码:

    SomeClass QueryServer(string args)
    {
        try
        {
            return SomeClass.Parse(_server.Query(args));
        }
        catch (Exception)
        {
            return null;
        }
    }

据我了解,抑制这样的错误是一个坏习惯,因为它会破坏原始服务器异常中的有用信息,并使代码在实际终止时继续执行。

何时才应该像这样完全消除所有错误?


13
埃里克·利珀特(Eric Lippert)撰写了一篇很好的博客文章,名为“ 烦恼异常”,他在其中将异常分为4个不同类别,并提出了针对每种异常的正确方法。我相信,这是从C#角度编写的,但适用于多种语言。
Damien_The_Unbeliever '16

3
我认为代码违反了“快速失败”原则。实际错误很可能隐藏在其中。
欣快感'16


4
您正在抓住很多东西。您还会捕获bug,例如NRE,数组索引超出范围,配置错误等。这会阻止您发现这些bug,并且它们会不断造成损害。
usr

Answers:


52

想象一下使用一堆库包含数千个文件的代码。想象所有的代码都是这样编码的。

想象一下,例如,服务器更新导致一个配置文件消失;现在,当您尝试使用该类时,所拥有的只是一个堆栈跟踪就是一个空指针异常:如何解决呢?可能要花费几个小时,至少要记录未找到的文件的原始堆栈跟踪[文件路​​径]才能使您立即解决。

甚至更糟糕的是:在更新之后使您使用的库中的一个失败,导致以后的代码崩溃。您如何将其追溯到图书馆?

即使没有强大的错误处理功能

throw new IllegalStateException("THIS SHOULD NOT HAPPENING")

要么

LOGGER.error("[method name]/[arguments] should not be there")

可以节省您的时间。

但是在某些情况下,您可能真的想忽略该异常并像这样返回null(或不执行任何操作)。特别是如果您与一些设计不良的旧代码集成在一起,并且通常情况下会出现此异常。

实际上,当您执行此操作时,您应该只想知道您是否真的是在忽略该异常或根据需要“适当处理”。如果在给定的情况下返回null将“正确处理”您的异常,请执行此操作。并添加一条评论为什么这是正确的选择。

最佳实践是大多数情况下可以遵循的事情,可能是80%,也许是99%,但是您总是会发现其中一种不适用的情况。在这种情况下,请留下评论,为什么您不遵循几个月后将阅读您的代码的其他人(甚至您自己)的做法。


7
+1可以解释为什么您认为需要这样做。如果没有任何解释,吞咽异常总会在我的脑海中发出警钟。
jpmc26 2016年

我的问题是注释停留在该代码内。作为该函数的使用者,我可能永远也看不到该评论。
corsiKa '16

@ jpmc26如果处理了异常(例如,通过返回一个特殊值),那根本就不是吞咽异常。
Deduplicator

2
@corsiKa如果功能正确执行,则消费者不需要知道实现细节。也许它们是apache库中的一部分,或者是spring,而您却一无所知。
Walfrat

@Deduplicator是的,但是使用catch作为算法的一部分也不应该是一个很好的实践:)
Walfrat

14

在某些情况下,此模式很有用-但是通常在不应生成异常的情况下(例如,将异常用于正常行为时)通常使用它们。

例如,假设您有一个打开文件的类,将其存储在返回的对象中。如果该文件不存在,则可以认为这不是错误情况,您返回null,然后让用户创建一个新文件。文件打开方法可能会在此处引发异常,以指示不存在任何文件,因此静默捕获它可能是有效的情况。

但是,您通常不希望这样做,并且如果代码中充斥着这种模式,您现在就想处理它。至少我希望这样一个静默的捕获能够写一条日志行,说明发生了什么(您已经进行了跟踪,对),并有注释来解释这种行为。


2
“永远不会在异常发生时使用”就没有这样的事情。异常要点是表示发生了严重错误。
欣快感'16

3
@Euphoric但是有时候图书馆的作者并不知道该图书馆的最终用户的意图。一个人的“严重错误”可能是其他人容易恢复的状况。
西蒙B

2
@Euphoric,这取决于您的语言认为“非常错误”的内容:Python人员喜欢异常,这些异常非常类似于(例如)C ++中的流控制。在某些情况下,返回null可能是有道理的-例如,从可能会突然且不可预测地逐出项目的缓存中读取数据。
马特·克劳斯

10
@Euphoric,“找不到文件不是正常的异常。如果可能,则应首先检查文件是否存在。” 没有!没有!没有!没有!关于原子操作,这是经典的错误思考。在检查文件是否存在与对其进行访问之间,可能会删除该文件。处理此类情况的唯一正确方法是访问异常并在异常发生时进行处理,例如,null如果这是您所选择的语言所能提供的最佳返回,则可以通过返回。
David Arno

3
@Euphoric:那么,编写代码来处理至少两次无法访问文件的问题?这违反了DRY,并且未执行的代码也是损坏的代码。
Deduplicator

7

这是100%与上下文相关的。如果此类函数的调用者要求在某处显示或记录错误,则显然是无稽之谈。如果调用方忽略了所有错误或异常消息,则返回异常或其消息没有多大意义。当要求使代码仅终止而不显示任何原因时,则调用

   if(QueryServer(args)==null)
      TerminateProgram();

可能就足够了。您必须考虑这是否会使用户难以找到错误的原因-如果是这种情况,这是错误处理的错误形式。但是,如果调用代码如下所示

  result = QueryServer(args);
  if(result!=null)
      DoSomethingMeaningful(result);
  // else ??? Mask the error and let the user run into trouble later

那么您应该在代码审查期间与开发人员争论。如果某人只是因为懒惰而无法找到正确的要求而实施了这种非错误处理形式,则不应将此代码投入生产,并且应该向代码的作者学习这种懒惰的可能后果。 。


5

在我花费大量时间进行程序设计和开发系统的那几年,我发现有问题的模式很有用(在两种情况下,压制还包含引发异常的日志记录,我不认为普通捕获和null返回是一种好习惯)。

两种情况如下:

1.当异常不被认为是例外状态时

这是当您对可能会抛出的某些数据执行操作时,您知道它可能会抛出,但仍希望您的应用程序继续运行,因为您不需要处理的数据。如果您收到它们,那很好,如果您没有,也很好。

可能会想到类的一些可选属性。

2.当您使用已经在应用程序中使用过的接口提供新的(更好,更快?)库的实现时

想象一下,您有一个使用某种旧库的应用程序,该库没有引发异常,但null在出错时返回。因此,您为此库创建了一个适配器,几乎复制了该库的原始API,并在您的应用程序中使用了这个新的(仍然是非抛出的)接口并null自己处理检查。

该库是一个新版本,或者可能是一个完全不同的库,它提供了相同的功能,而不是返回nulls 而是引发异常,而您要使用它就可以使用它。

您不想将异常泄漏到主应用程序,因此您可以阻止这些异常并将其记录在您创建的用于包装此新依赖项的适配器中。


第一种情况不是问题,这是代码的预期行为。但是,在第二种情况下,如果到处都是null库适配器的返回值确实意味着有错误,那么重构API以引发异常并捕获该异常而不是进行检查null可能是一个好主意(通常在代码方面也是如此)。

我个人仅在第一种情况下使用异常抑制。我仅在第二种情况下使用了它,当时我们没有预算来使应用程序的其余部分使用nulls 而不是使用例外。


-1

可以合理地说,程序应该只捕获它们知道如何处理的异常,并且可能不知道如何处理程序员未预料到的异常,但是这样的说法忽略了许多操作可能会失败的事实。几乎无数种没有副作用的方法,而且在许多情况下,对绝大多数此类故障的正确处理将是相同的;失败的确切细节将是无关紧要的,因此,程序员是否预期到失败无关紧要。

例如,如果功能的目的是将文档文件读入合适的对象,然后创建一个新的文档窗口以显示该对象,或者向用户报告无法读取该文件,则尝试加载无效的文档文件不应使应用程序崩溃-而是应显示一条指示问题的消息,但应让应用程序的其余部分继续正常运行,除非出于某种原因尝试加载文档已破坏了系统中其他内容的状态。

从根本上说,对异常的正确处理通常取决于对异常类型的依赖,而不是引发异常的位置。如果资源由读写锁保护,并且在获取了用于读取的锁的方法中引发了异常,则正确的行为通常应该是释放锁,因为该方法无法对资源执行任何操作。如果在获取锁以进行写入时抛出了异常,则由于受保护的资源可能处于无效状态,因此锁通常应无效。如果锁定原语不具有“无效”状态,则应添加一个标志来跟踪这种无效。在不使锁无效的情况下释放该锁是不好的,因为其他代码可能会将受保护的对象视为无效状态。但是,让锁悬而未决是' 一个适当的解决方案。正确的解决方案是使锁无效,以便任何未决或将来的获取尝试都将立即失败。

如果事实证明无效资源在尝试使用它之前就被放弃了,那么就没有理由关闭该应用程序。如果无效的资源对​​于应用程序的继续运行至关重要,则需要关闭该应用程序,但是使资源无效很可能会导致这种情况的发生。收到原始异常的代码通常无法知道哪种情况适用,但是如果使资源无效,则可以确保无论哪种情况都将采取正确的措施。


2
这无法回答该问题,因为在不知道异常详细信息的情况下处理异常!=会静默使用(通用)异常。
Taemyr

@Taemyr:如果一个函数的目的是“尽可能执行X,否则表明它不是”,而X则不可能,那么列出各种异常既不可行又不可行呼叫者也不会关心“不可能做X”。口袋妖怪异常处理通常是使事情在这种情况下正常工作的唯一实用方法,并且如果代码受到约束以使由于意外异常而损坏的事物无效,则不必像某些人所指出的那样邪恶。
超级猫

究竟。每个方法都应该有一个目的,如果它能够实现其目的,而不管它调用的某个方法是否引发异常,那么吞下该异常,不管它是什么,然后执行它的工作,都是正确的选择。错误处理从根本上讲就是在发生错误后完成任务。那才是重要的,而不是发生了什么错误。
jmoreno'3

@jmoreno:使事情变得困难的是,在许多情况下,会有大量的异常,其中许多异常是程序员不会特别预料到的,这并不表示有问题,还有一些异常,其中有些异常是程序员不会没有特别预料到的,但确实预示了问题,也没有明确的系统方法来区分它们。我唯一明智地处理此问题的方法是让异常使已损坏的事物无效,因此,如果它们很重要,它们就会被注意,如果不重要,它们将被忽略。
超级猫

-2

吞下这种错误对任何人都不是特别有用。是的,原始呼叫者可能不在乎该异常,但其他人可能会在意。

那他们怎么办?他们将添加代码来处理异常,以查看返回的异常类型。大。但是,如果保留了异常处理程序,则原始调用者将不再返回null,并且其他地方会中断。

即使您知道某些上游代码可能会引发错误并因此返回null,也不会对您造成严重的干扰,至少不要试图阻止调用代码IMHO中的异常。


“严重惰性”-您可能是说“严重惰性”吗?
深奥的屏幕名称

@EsotericScreenName选择一个... ;-)
罗比·迪

-3

我已经看到了第三方库具有潜在有用方法的示例,除了在某些情况下它们会抛出异常外,它们对我有用。我能做什么?

  • 实现我自己需要的东西。在最后期限可能会很困难。
  • 修改第三方代码。但是随后我必须在每次升级时寻找合并冲突。
  • 编写一个包装方法,将异常转换为普通的空指针,布尔布尔值false或其他任何形式。

例如,库方法

public foo findFoo() {...} 

返回第一个foo,但是如果没有,则抛出异常。但是我需要的是一种返回第一个foo或null的方法。所以我写这个:

public foo myFindFoo() {
    try {
        return findFoo() 
    } 
    catch (NoFooException ex) {
        return null;
    }
}

不整齐。但有时务实的解决方案。


1
您的意思是“在应该为您工作的地方抛出异常”?
corsiKa

@corsiKa,我想到的示例是通过列表或类似数据结构搜索的方法。它们什么都不找到会失败还是返回空值?另一个示例是waitUntil方法,该方法在超时时失败,而不是返回布尔值false。
om
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.