您如何看待“不要抓住意外的异常”最佳做法?

时间:2009-02-25 11:43:22

标签: architecture exception-handling

我已经读了很多次,不应该盲目地捕捉异常。 有人说可以将Main()包装成一个catch块来显示错误,而不是只退出(see this SO post for example),但似乎有一种共识,即如果出现意外情况,你绝不应让程序运行,因为它处于未知状态,并且可能以意想不到的方式发挥作用。

虽然我同意隐藏错误而不是修复错误这一事实绝对是错误的想法,但请考虑以下情况:

你有一个庞大的服务器。百万行代码。

启动时,它会将所有客户加载到其本地缓存中。

对我来说,写这个很有意义:

           foreach (string CustomerID in Customers)
                try
                {
                    LoadCustomer(CustomerID);
                }
                catch (Exception ex) // blind catch of all exceptions
                {
                    // log the exception, and investigate later.

                }

如果没有盲目捕获,未能加载单个客户只会使所有服务器崩溃。

我绝对更喜欢让我的服务器对一个客户产生一些未知的副作用,而不是整个服务器。

当然,如果我运行我的catch代码,我要做的第一件事就是修复代码中的错误。

我有什么东西可以在这里俯瞰吗?是否有已知的最佳实践(除了“永不捕获意外异常”策略?)

最好在LoadCustomer()方法中捕获异常,从那里重新抛出'CustomerLoadException',并在调用代码中捕获CustomerLoadException而不是所有Exception?

14 个答案:

答案 0 :(得分:13)

这是健壮性的问题,即使出现无法解释的错误也会继续运行,而正确性,更愿意完全失败而不是产生不可靠的结果。

显然,如果您正在使用生命支持系统,那么您不希望系统因未知错误情况而简单地关闭。即使你的状态没有明确定义,也要进行操作可能比终止更好。

另一方面,如果您的程序是购物车,最好完全失败而不是潜在地发送错误的项目,或向错误的个人收取错误的金额。

介于两者之间的一切都是灰色区域,这是一个判断。总的来说,生命支持系统编程似乎比购物车编程更为罕见,因此广泛的建议是让人们做最常见的事情,即在发生意外错误时失败。据了解,如果您正在处理不合适的案例(例如您的服务器示例),您会更清楚。

答案 1 :(得分:2)

“我有什么东西在这里俯瞰吗?”

“是否有已知的最佳实践(除了'永远不会捕获意外异常'策略'?)”

是。封装。责任分配。 S. O. L. I. D.原则。

最好在LoadCustomer()方法中捕获异常,从那里重新抛出'CustomerLoadException',并在调用代码中捕获CustomerLoadException而不是所有Exception?

是。这封装了提供loadCustomer方法的类定义中可能出错的所有内容。

答案 2 :(得分:2)

我认为这取决于Senario,但作为一项规则,我只能捕获异常,我希望通过日常使用应用程序来使用,并使用某种未经验证的异常报告/日志记录工具,例如ASP.Net的健康监控。但是,如果它是应用程序的一个关键部分,根本无法获得无法解决的异常,我会捕获所有异常并再次报告/记录它们。

答案 3 :(得分:1)

这是一个逐案的事情。如果您可以保证加载失败对其他客户没有任何影响,那么记录和跳过该特定客户似乎是合理的。但并非所有异常都以这种方式工作。特别是对于错误条件,例如VirtualMachineError,问题通常会影响整个程序运行,并且应该中止。

答案 4 :(得分:1)

我绝对更喜欢让我的服务器对一个客户产生一点未知的副作用,而不是整个服务器。

你怎么知道在LoadCustomer()中导致异常的错误没有其他任何东西?总的来说,我认为我更喜欢“日志异常,将异常重新抛到更高级别”,并且可能会退出。

在这个特定情况下,我可能会争论两种处理方式,但总的来说,处理错误而你没有明确的处理方法并不理想。

答案 5 :(得分:1)

我认为您缺少的是桌面应用程序的“最佳实践”不一定与服务器的“最佳实践”相同,这与网页的不同。

如果网页引发未处理的异常,您会想到“meh”并点击刷新。如果桌面应用程序抛出一个,您可能会丢失一些数据并且非常恼火。如果服务器抛出一个,那么整个业务可能会停止,直到它被修复为止。

它还取决于修补程序的容易程度 - 如果您可以轻松地将修补程序推送到所有安装程序(例如您自己的服务器或内部应用程序),那么最好不要保留异常。如果您正在编写无法修补的内容(适用于较旧控制台或嵌入式设备应用程序的游戏),那么您可能更好地吞下异常。

当然,在所有情况下,最好让您的测试能够解决问题,以便您能够正确处理它,但我们都知道错误会发生......

答案 6 :(得分:1)

抓住你所知道的例外情况非常好。您可能期望在示例中出现NullObjectException。抓住这是完全合理的。

当您捕获一般异常时,您实际上是在说“我不知道出了什么问题,或者服务器是否着火,我不关心”。虽然您的服务器可能会继续运行,但无法确定内部状态是什么,或者此异常是否会导致崩溃。

此外,通过此时捕获任意异常,如果稍后发生崩溃,您将远离“真正”错误发生的位置 - 这将使查找错误更加困难。

类似地,虽然没有捕获通用异常可能会导致立即崩溃,但是崩溃更容易在开发周期的早期被捕获,并且很容易修复(因为您将知道错误实际发生的位置)。

捕捉异常真的没有优势。

答案 7 :(得分:0)

为了解决真正影响我的程序稳定性的异常,也许编写这种代码是个好主意:

      foreach (string CustomerID in Customers)
            try
            {
                LoadCustomer(CustomerID);
            }
            catch (Exception ex) // blind catch of all exceptions
            {
                if (ex is OutOfMemoryException || ex is StackOverflowException ||...)
                     throw;
                // log the exception, and investigate later.

            }

答案 8 :(得分:0)

我在这方面不是很有经验。但是,在我看来: 1.几乎所有规则都有一些例外。知道何时违反规则很重要。 2.在异常处理的情况下,盲目捕获异常几乎不是一个好主意。这是因为您可能会发现一些非常意外的错误。

例如,在Python(至少2.5.2)中,盲目捕捉将允许我捕获ctrl + c信号(Linux)。这意味着我无法在紧急情况下终止申请。

  1. 对于您的网络服务器代码,您可能需要执行以下操作之一 +如果客户无法加载并记录/更正该异常,请使用引发的自定义异常 +在更深层次的代码中使用异常处理并在那里处理它。

答案 9 :(得分:0)

好吧,让我这样说..说神秘的客户无法加载,因为它不存在,比如因为有人已经想出如何破解你的系统的前层。现在想象一下,黑客会做各种恶意的事情,除非他碰巧触发了一些试图从客户的未知价值中读取的功能,但是现在系统会崩溃,但是黑客就能够与无效客户一起做各种各样的中断。

这真的更好吗?

答案 10 :(得分:0)

当您知道哪些对象和资源可能会受到影响并丢弃所有这些对象时,您可以很好地捕获例外情况(不是错误)。

因此,在您的情况下,可能可以处理将整个过程转换为noop的异常。但你必须确保: - 没有其他共享资源是有影响的(例如,在异常后可能已经死亡的Hibernate会话) - 完整(反式)动作不仅取消了一半。这意味着这样的异常处理可能只出现在特殊的地方,通常直接在“用户界面”之下。示例:用户按下应该加载客户的按钮,更改其地址并再次保存。如果出现任何错误,您可能会发现异常,阻止所有剩余步骤发生,抛弃客户对象并显示一条消息:对不起,没有用。

但是如果你发现异常(例如在修改地址期间)然后继续工作,那么用户拥有客户对象的东西就会在他的控制下改变地址,而实际上他控制的是具有破坏地址的客户对象。不太好。

所以规则是:定义控制事务的层。这应该是捕获异常的层。可能是SwingActions,可能是Threads,可能是public void main,可能在你的例子中的循环内。

答案 11 :(得分:0)

我之前通过捕捉一般的“例外”而不是个人类型而被抓住了。这意味着你丢失了一些关于实际出错的信息。您通常希望以不同方式处理每种类型的异常。

加上顶级异常处理只应用于记录错误并重新启动应用程序。如果您不知道什么类型的异常导致程序可能处于无效状态,那么您可能会因为内存不足或其他原因而无法使用。

答案 12 :(得分:0)

这取决于你站在哪里。如果你处于较低的水平,你可以预见的大多数例外,你应该只明确地捕捉那些,并留下其余的泡沫。当您到达GUI或顶级逻辑时,将捕获所有已知异常,并且最终只会出现意外异常。现在您有两个选择:

  1. 悲观看法:显示异常并放弃。
  2. 乐观的观点:你有信心你已经抓住了所有评论家的例外情况,所以你放弃了例外并继续(ON ERROR RESUME NEXT,任何人?)
  3. 无论哪种方式,您都要记录异常,并确保定期检查日志。

    最后,这是一个“全部依赖”的情况。更糟的是什么?继续输入错误数据或意外退出?这是一个关键系统吗?是否是其他系统使用的输出?等等。

    无论如何,所有关键异常已被捕获到应用程序的更深层次。

    如果感谢日志,您会发现未处理的异常,请更正代码,进行测试并重新部署。

答案 13 :(得分:0)

许多语言中的异常层次结构的一个主要问题是异常按类型或通过调用类来组织,而不是按严重性组织。通常在抛出异常时,这是因为在执行任何操作之前确定其前提条件未得到满足的方法。抛出异常的第一个例程的调用者可能会或可能不会在该点执行任何重要操作。在许多情况下,调用者在调用例程之前所做的任何操作都只会对临时变量起作用,当异常冒泡时,这些变量将会消失。因此,在绝大多数情况下,当类似于LoadDocument()例程的事件抛出异常时,加载文档的尝试将对仍然存在的任何内容的状态没有影响。如果加载文档的失败尝试没有副作用,并且应用程序准备处理文档未加载的事实,则没有理由不继续应用程序。困难在于知道是否存在任何副作用。

现有的异常处理系统中缺少的一个功能是Exception中的虚拟IsResolved属性。在处理“catch”块之后,系统将检查IsResolved是否返回false。如果它为非null并返回true,则会调用UnresolvedExceptionException,前一个异常为InnerException(UnresolvedExceptionException.IsResolved将调用其InnerException的IsResolved方法)。

大多数IsResolved实现将测试返回Boolean的函数的委托列表是否为空;如果不是,如果任何列出的委托返回True,则返回true。如果异常意味着未处置的对象已损坏,则IsResolved方法可以返回该对象的Disposed标志。如果对象已被处置,其腐败将不再相关。如果对象被包装在Using块中,这种行为会导致异常渗透到对象被处置的位置,但不会再进一步​​。

像OutOfMemoryException这样的非常讨厌的异常只能通过显式调用OutOfMemoryException.AcknowledgeOutOfMemoryCondition来使其IsResolved方法变得高兴。抛出异常的许多其他例程会使IsResolved方法立即返回true(例如,由于找不到键,因此会抛出Dictionary.GetValue,那么就字典而言,抛出异常时会立即解决异常)

不幸的是,我不确定如何将这样的方案添加到像.net这样的现有平台上。尽管如此,如果将来有人开发出类似的系统,那么包含它似乎是一个有用的功能。