Java或C#中异常管理的最佳实践


117

我一直在决定如何处理应用程序中的异常。

如果我的异常问题很大程度上来自于1)通过远程服务访问数据或2)反序列化JSON对象。不幸的是,我不能保证其中任何一项都能成功(切断网络连接,无法控制的畸形JSON对象)。

结果,如果确实遇到异常,我将在函数中捕获该异常并将FALSE返回给调用方。我的逻辑是,调用者真正关心的只是任务是否成功,而不是为什么任务没有成功。

这是典型方法的一些示例代码(在JAVA中)

public boolean doSomething(Object p_somthingToDoOn)
{
    boolean result = false;

    try{
        // if dirty object then clean
        doactualStuffOnObject(p_jsonObject);

        //assume success (no exception thrown)
        result = true;
    }
    catch(Exception Ex)
    {
        //don't care about exceptions
        Ex.printStackTrace();
    }
    return result;
}

我认为这种方法很好,但是我真的很想知道管理异常的最佳实践是什么(我真的应该一直在调用堆栈中冒泡一个异常吗?)。

关键问题总结:

  1. 可以只捕获异常但不将其冒泡或正式通知系统(通过日志或向用户的通知)可以吗?
  2. 有什么最佳实践可以解决并非导致所有内容都需要try / catch块的异常?

跟进/编辑

感谢您提供的所有反馈,在线找到了一些有关异常管理的出色资源:

异常管理似乎是随上下文而变化的事物之一。但最重要的是,它们在系统内如何管理异常应该保持一致。

另外,还要通过过多的try / catching来注意代码是否腐烂,或者不要对异常给予尊重(异常警告系统,还有什么需要警告的?)。

另外,这是m3rLinEz的不错选择。

我倾向于同意安德斯·海斯伯格(Anders Hejlsberg)和您的看法,大多数致电者只关心操作是否成功。

通过此注释,它提出了一些在处理异常时要考虑的问题:

  • 抛出该异常有什么意义?
  • 处理它有什么意义?
  • 呼叫者是否真的在乎异常,还是在乎通话是否成功?
  • 强制调用方正常管理潜在异常吗?
  • 您是否尊重语言的本质?
    • 您是否真的需要返回布尔值等成功标志?返回布尔值(或整数)比C语言更像C语言(在Java中,您将处理异常)。
    • 遵循与语言相关的错误管理结构:)!

尽管oracle社区的文章是用Java编写的,但它是针对非常广泛的一类语言的通用建议。不错的文章,正是我想要的。
兔子兔子

Answers:


61

您似乎想捕获异常并将其转换为错误代码,这对我来说似乎很奇怪。当Java和C#中的默认值是异常时,为什么您认为调用者更喜欢错误代码?

至于你的问题:

  1. 您应该只捕获可以实际处理的异常。在大多数情况下,仅捕获异常并不是正确的选择。有一些例外(例如,线程之间的日志记录和编组例外),但即使是那些情况,您也通常应该重新抛出这些例外。
  2. 您的代码中绝对不应包含很多try / catch语句。同样,该想法是仅捕获可以处理的异常。您可以包括一个最顶层的异常处理程序,以将任何未处理的异常转换成对最终用户有用的东西,但是否则,您不应尝试在每个可能的位置捕获每个异常。

至于为什么有人想要错误代码而不是异常……我一直认为,即使我的应用程序正在生成异常,HTTP仍然使用错误代码是很奇怪的。为什么HTTP无法让我按原样传递异常?
Trejkaz 2012年

最好的办法是将错误代码包装在monad中
lindenrovio 2014年

@Trejkaz您不想将异常详细信息返回给用户,因为这存在安全风险。这就是HTML服务器返回错误代码的原因。返回错误消息也是一个本地化问题,可能会使HTML变得更大而返回更慢。所有这些都是我认为HTML服务器返回错误代码的原因。
Didier A.

我认为最好说:“您只能抑制实际上可以处理的异常。”
Didier A.

@didibus为什么您认为安全问题会给开发带来不便?关于生产我什么也没说。至于错误消息的本地化,整个网站已经存在该问题,人们似乎正在应对。
Trejkaz 2014年

25

这取决于应用程序和情况。如果要构建库组件,则应使异常冒泡,尽管应将异常包装为与组件相关。例如,如果您构建一个Xml数据库,并且假设您正在使用文件系统来存储数据,并且正在使用文件系统权限来保护数据。您不希望冒泡FileIOAccessDenied异常,因为它会泄漏您的实现。相反,您将包装异常并引发AccessDenied错误。如果您将组件分发给第三方,则尤其如此。

至于吞下异常是否可以。这取决于您的系统。如果您的应用程序可以处理失败的情况,并且没有通知用户失败的原因,则继续进行,尽管我极力建议您将失败记录下来。我总是发现调用它来解决问题并发现他们吞没了异常(或者替换它并抛出一个新异常而不设置内部异常)令人沮丧。

通常,我使用以下规则:

  1. 在我的组件和库中,只有在我打算处理或基于异常进行处理时,我才会捕获异常。或者,如果我想在例外情况下提供其他上下文信息。
  2. 我在应用程序入口点或可能的最高级别使用常规try catch。如果出现异常,我将其记录下来并让其失败。理想情况下,异常永远不应该到达这里。

我发现以下代码有异味:

try
{
    //do something
}
catch(Exception)
{
   throw;
}

这样的代码毫无意义,不应包含在内。


@Josh,关于吞下异常的要点,但我相信在少数情况下仅吞下异常是可以接受的。在上一个项目中,您的代码段被强制执行了。记录它们使情况变得更糟,我的建议,如果您无法处理异常,请不要吞下它。
smaclell,2009年

就像我说的那样,这全都取决于应用程序和特定的上下文。有时我会吞下异常,尽管这种情况很少见,但我不记得上一次这样做的时候;-)可能是当我编写自己的记录器,写入日志以及辅助日志失败时。
JoshBerke,2009年

该代码提供了一个要点:您可以在“ throw”处设置一个断点。
Rauhotz

3
弱点是,您可以告诉VS在抛出任何异常时中断,也可以缩小范围并选择特定的异常。在VS2008中,有一个菜单项处于调试状态(您需要自定义工具栏才能找到它)调用的异常
JoshBerke,2009年

即使有这种简单形式,“代码气味”示例也具有一定的副作用。如果在抛出的点周围// do something包括任何try/finally块,则这些finally块将在该catch块之前执行。如果没有使用try/catch,则异常将一直飞到堆栈的顶部,而finally不会执行任何块。这使顶级处理程序可以决定是否执行这些finally块。
Daniel Earwicker 2010年

9

我想推荐有关该主题的另一个很好的资源。这是分别针对C#和Java的发明者,Anders Hejlsberg和James Gosling进行的有关Java的检查异常的访谈。

失败与例外

页面底部也有大量资源。

我倾向于同意安德斯·海斯伯格(Anders Hejlsberg)和您的看法,大多数致电者只关心操作是否成功。

Bill Venners:您提到了与已检查异常有关的可伸缩性和版本控制问题。您能否说明这两个问题是什么意思?

Anders Hejlsberg:让我们从版本控制开始,因为在那里很容易看到问题。假设我创建了一个声明为foo的方法,该方法将引发异常A,B和C。在foo的第二版中,我想添加一堆功能,而现在foo可能会引发异常D。将D添加到该方法的throws子句中,因为该方法的现有调用者几乎肯定不会处理该异常。

在新版本的throws子句中添加新的异常会破坏客户端代码。这就像向接口添加方法。发布接口后,出于所有实际目的,该接口是不可变的,因为该接口的任何实现都可能具有要在下一版本中添加的方法。因此,您必须创建一个新界面。与异常类似,您将必须创建一个称为foo2的全新方法,该方法将引发更多异常,或者必须在新foo中捕获异常D,然后将D转换为A,B或C。

Bill Venners:但是,即使是在没有经过检查的异常的语言的情况下,您是否也无论如何都不会破坏他们的代码?如果新版本的foo将引发客户应考虑处理的新异常,那么他们的代码是否仅仅因为在编写代码时就没有期望该异常而被破坏?

Anders Hejlsberg:不,因为在很多情况下,人们不在乎。他们不会处理任何这些异常。他们的消息循环周围有一个底层异常处理程序。该处理程序将只弹出一个对话框,指出出了什么问题并继续。程序员通过在各处编写try finally来保护代码,因此,如果发生异常,他们将正确退出,但是实际上他们对处理异常并不感兴趣。

throws子句(至少是用Java实现的方法)不一定会迫使您处理异常,但是如果您不处理异常,它将迫使您准确地确认可能会通过哪些异常。它要求您要么捕获已声明的异常,要么将其放入自己的throws子句中。要解决此要求,人们会做一些荒谬的事情。例如,它们用“引发异常”来修饰每个方法。这完全破坏了该功能,并且您使程序员编写了更多乱七八糟的东西。那对任何人都没有帮助。

编辑:在对话中添加了更多详细信息


谢谢你!我用有关您答案的信息更新了我的问题!
AtariPete

听起来Hejlsberg先生正在为Pokemon异常处理辩护。Java和C#中的异常设计的巨大问题之一是,过多的信息以异常的类型进行编码,而应将信息存储在异常实例中的方式却无法以任何一致的方式提供。异常应该在调用堆栈中传播,直到所有代表的异常情况都得到解决为止。不幸的是,即使已识别,异常的类型也无法指示情况是否已解决。如果无法识别例外...
超级猫

情况甚至更糟 如果代码调用FetchData并抛出意外类型的异常,则它无法知道该异常是否仅表示数据不可用(在这种情况下,没有它的代码获得的能力将“解决”它),或者这是否意味着CPU着火了,系统应该在第一时间执行“安全关闭”操作。听起来像海斯伯格先生在暗示,代码应采用前者;考虑到现有的异常层次结构,这也许是最好的策略,但这似乎还很棘手。
2013年

我同意抛出Exceptin是荒谬的,因为几乎所有东西都可以抛出异常,所以您应该假设这会发生。但是,当您指定抛出A,B,C之类的异常时,对我来说,它只是一个注释,应像Comment块一样使用。这就像说,嘿,我的功能客户,这里有个提示,也许您想处理A,B,C,导致使用我时可能发生这些错误。如果您在将来添加d,它不是一个大问题,如果它没有被处理,但它像的方式添加新的文件,呵呵,现在它也将是有益的捉D.
迪迪埃·答:

8

一般而言,检查异常是一个有争议的问题,特别是在Java中(稍后我将尝试为赞成和反对的人找到一些示例)。

作为经验法则,异常处理应该围绕这些准则进行,并且没有特定的顺序:

  • 为了可维护性,请始终记录异常,以便在您开始查看错误时,日志将帮助您指向错误可能开始的地方。永远不要离开printStackTrace()或喜欢它,很可能您的用户之一最终将获得那些堆栈跟踪中的一个,并且对使用它一无所知
  • 捕获只能处理的异常,并处理它们,而不仅仅是将它们扔到栈上。
  • 总是捕获特定的异常类,通常您绝不应该捕获type Exception,否则很可能会吞下其他重要的异常。
  • 永不(抓住)Error,意思是:切勿捕获Throwables,因为Errors是后者的子类。Error是您很可能无法处理的问题(例如OutOfMemory或其他JVM问题)

关于您的具体情况,请确保任何调用您的方法的客户端都将收到正确的返回值。如果失败,则返回布尔值的方法可能返回false,但请确保调用该方法的位置能够处理该错误。


在C#中,检查异常不是问题,因为它没有异常。
cletus

3
恕我直言,有时它可以很好地捕获错误:我忘了写的一个非常占用内存的Java应用程序。我捕获了OutOfMemory-Ex,并向用户显示一条消息,告知他内存不足,他应该退出其他程序,并告诉他如何使用分配的更多堆空间启动jvm。我想这很有帮助。
Lena Schimmel

5

您应该只捕获可以处理的异常。例如,如果您正在处理通过网络进行的读取,并且连接超时并且遇到异常,则可以重试。但是,如果您正在通过网络阅读并获得IndexOutOfBounds异常,则您将无法处理该异常,因为您不知道是什么原因(在这种情况下,您不会)。如果要返回false或-1或null,请确保它用于特定的异常。我不希望我使用的库在抛出异常是堆内存不足时在网络读取时返回false。


3

异常是不属于正常程序执行的错误。根据程序的功能及其用途(即字处理器与心脏监护仪),遇到异常时,您将希望做不同的事情。我使用过将异常作为正常执行的一部分的代码,这绝对是代码的味道。

例如

try
{
   sendMessage();

   if(message == success)
   {
       doStuff();
   }
   else if(message == failed)
   {
       throw;
   }
}
catch(Exception)
{
    logAndRecover();
}

此代码使我烦恼。IMO,除非它是一个关键程序,否则您不应从异常中恢复。如果抛出异常,那么坏事就发生了。


2

以上所有情况似乎都是合理的,并且通常您的工作场所可能会制定政策。在我们这里,我们已经定义了Exception类型:(SystemException未选中)和ApplicationException(选中)。

我们已经同意,SystemExceptions不太可能被恢复,并且将在顶端处理一次。为了提供进一步的情况下,我们的SystemExceptions的exteneded说明他们发生地,例如RepositoryExceptionServiceEception等等。

ApplicationException可能具有类似的业务含义InsufficientFundsException,应由客户代码处理。

Witohut是一个具体的示例,很难对您的实现进行评论,但是我永远不会使用返回码,因为它们是维护问题。您可能会吞下一个Exception,但是您需要确定原因,并始终记录事件和stacktrace。最后,由于您的方法没有其他处理,因此它是相当多余的(除了封装?),因此doactualStuffOnObject(p_jsonObject);可以返回布尔值!


1

经过一番思考并查看您的代码,在我看来,您只是将异常作为布尔值重新抛出。您可以只让该方法通过此异常(您甚至不必捕获它)并在调用方中对其进行处理,因为这是重要的地方。如果异常将导致调用方重试此功能,则调用方应为捕获异常的调用方。

有时可能会遇到这样的情况,即您遇到的异常对调用方而言没有意义(即,这是网络异常),在这种情况下,您应该将其包装在特定于域的异常中。

另一方面,如果异常表示您的程序中出现不可恢复的错误(即,此异常的最终结果将是程序终止),我个人想通过捕获它并抛出运行时异常来使其明确。


1

如果要在示例中使用代码模式,则将其称为TryDoSomething,并仅捕获特定的异常。

还可以考虑使用异常过滤器在出于诊断目的记录异常时,。VB具有对异常过滤器的语言支持。指向Greggm博客的链接具有可从C#使用的实现。异常过滤器具有更好的属性,可通过捕获和重新抛出进行调试。具体来说,您可以将问题记录在过滤器中,并让异常继续传播。该方法允许附加JIT(即时)调试器以具有完整的原始堆栈。重新投掷会在重新投掷时切断堆栈。

在包装第三方函数时,TryXXXX很有用,这种情况是在并非真正异常的情况下抛出的,或者在不调用该函数的情况下很难测试的情况。例如:

// throws NumberNotHexidecimalException
int ParseHexidecimal(string numberToParse); 

bool TryParseHexidecimal(string numberToParse, out int parsedInt)
{
     try
     {
         parsedInt = ParseHexidecimal(numberToParse);
         return true;
     }
     catch(NumberNotHexidecimalException ex)
     {
         parsedInt = 0;
         return false;
     }
     catch(Exception ex)
     {
         // Implement the error policy for unexpected exceptions:
         // log a callstack, assert if a debugger is attached etc.
         LogRetailAssert(ex);
         // rethrow the exception
         // The downside is that a JIT debugger will have the next
         // line as the place that threw the exception, rather than
         // the original location further down the stack.
         throw;
         // A better practice is to use an exception filter here.
         // see the link to Exception Filter Inject above
         // http://code.msdn.microsoft.com/ExceptionFilterInjct
     }
}

是否使用像TryXXX这样的模式更多是样式问题。捕获所有异常并吞下它们的问题不是样式问题。确保允许意外的异常传播!


我喜欢.net中的TryXXX模式。
2009年

1

我建议从标准库中获取您所使用语言的提示。我不能代表C#,但让我们看一下Java。

例如,java.lang.reflect.Array有一个静态set方法:

static void set(Object array, int index, Object value);

C方式是

static int set(Object array, int index, Object value);

...返回值为成功指示。但是您不再处于C语言世界中。

一旦您接受了异常,您就会发现通过将错误处理代码从核心逻辑中移开,它使您的代码更简单,更清晰。旨在在一个try块中包含许多语句。

正如其他人指出的那样,在捕获的异常方面,您应该尽可能具体。


这是一个非常有效的评论,请尊重该语言以及它在传统上如何处理此类问题。不要将C语言观念带入Java世界。
AtariPete

0

如果您要捕获一个Exception并返回false,则它应该是一个非常特殊的异常。您没有这样做,而是捕获了所有这些并返回false。如果我收到MyCarIsOnFireException,我想马上知道!我可能不在乎的其余异常。因此,您应该有一堆异常处理程序,它们对某些异常(“抛出,或者捕获并重新抛出新的异常,说明发生了什么情况”)说“谁在哪里错了”,而对于其他异常则返回false。

如果您要启动的是该产品,则应将这些异常记录在某处,它将帮助您将来进行调整。

编辑:关于将所有内容包装在try / catch中的问题,我认为答案是肯定的。异常在您的代码中应该很少见,以至于catch块中的代码很少执行,以至于根本不影响性能。例外应该是您的状态机损坏并且不知道该怎么做的状态。至少抛出一个异常,该异常解释了当时发生的情况,并且在其中包含了捕获的异常。对于必须弄清楚为什么在度假(或新工作)时发生故障的人,“ doSomeStuff()方法中的异常”并没有太大帮助。


不要忘记,设置异常块也
要付出代价

0

我的策略:

如果原始函数返回void,则将其更改为bool。如果发生异常/错误,则返回false;如果一切正常,则返回true

如果该函数应返回某些内容,则在发生异常/错误时返回null,否则返回可返回项。

取而代之的布尔一个字符串可能包含错误的描述被退回。

在每种情况下,在返回任何内容之前,请记录错误。


0

这里有一些很好的答案。我想补充一点,如果您最终得到的东西与您张贴的一样,至少要比堆栈跟踪打印更多。说一下您当时的工作,然后说出Ex.getMessage(),给开发人员一个奋斗的机会。


我完全同意。我只做了Ex.printStackTrace(); 作为我在抓捕中所做的事情的一个示例(即不重新投掷)。
AtariPete

0

try / catch块形成嵌入在第一组(主)集中的第二组逻辑,因此,它们是删除难以理解,难以调试的意大利面条代码的好方法。

尽管如此,合理地使用它们在可读性方面仍能创造奇迹,但是您应该遵循两个简单的规则:

  • 在底层使用(分别)使用它们来捕获库处理问题,并将它们流回到主逻辑流程中。我们想要的大多数错误处理应该来自代码本身,作为数据本身的一部分。如果返回的数据不是特殊的,为什么要设置特殊条件呢?

  • 在较高级别使用一个大处理程序来管理代码中未在较低级别捕获的任何或所有怪异条件。对错误进行一些有用的处理(日志,重新启动,恢复等)。

除了这两种类型的错误处理之外,中间的所有其余代码都应释放,并且没有try / catch代码和错误对象。这样,无论您在何处使用它,或对其进行何种操作,它都可以简单且按预期工作。

保罗


0

我的答案可能有点晚了,但是错误处理是我们可以随时改变和发展的东西。如果您想了解更多有关此主题的内容,我在新博客中写了一篇有关此主题的文章。http://taoofdevelopment.wordpress.com

快乐的编码。

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.