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

Best practices for exception management in Java or C#

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

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

因此,如果我遇到异常,我只需在函数内捕获它并返回FALSE给调用者。我的逻辑是,所有调用者真正关心的是任务是否成功,而不是为什么它不成功。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
}

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

总结关键问题:

  • 是否可以捕获异常但不会冒泡或正式通知系统(通过日志或通知用户)?
  • 对于不会导致所有需要try / catch块的异常的最佳实践有哪些?
  • 跟进/编辑

    感谢所有反馈,在网上找到了一些关于异常管理的优秀来源:

    • 异常处理的最佳实践|奥莱利媒体
    • .NET中的异常处理最佳实践
    • 最佳实践:异常管理(文章现在指向archive.org副本)
    • 异常处理反模式

    似乎异常管理是根据上下文而变化的事情之一。但最重要的是,人们应该如何管理系统中的异常。

    另外注意通过过多的尝试/捕获代码腐烂或不给予例外它的尊重(例外是警告系统,还需要警告什么?)。

    此外,这是m3rLinEz的一个很好的选择评论。

    I tend to agree with Anders Hejlsberg and you that the most callers only
    care if operation is successful or not.

    从这个评论中,它提出了一些在处理异常时要考虑的问题:

    • 抛出此异常有什么意义?
    • 处理它有什么意义?
    • 呼叫者是否真的关心异常,还是只关心呼叫是否成功?
    • 是否强制调用者管理潜在的异常优雅?
    • 你是否尊重这种语言的含义?

      • 你真的需要返回像布尔这样的成功标志吗?返回boolean(或int)更像是一种C心态而不是Java(在Java中你只是处理异常)。
      • 遵循与语言相关的错误管理结构:)!


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

    至于你的问题:

  • 您应该只捕获实际可以处理的异常。只是
    在大多数情况下,捕获异常并不是正确的做法。
    有一些例外(例如,日志记录和编组异常
    线程之间)但即使对于那些情况你通常应该
    重新抛出异常。
  • 你绝对不应该有很多try / catch语句
    码。同样,我们的想法是只捕获您可以处理的异常。
    您可以包含一个最顶层的异常处理程序来转换任何未处理的
    对最终用户有用的异常但是
    否则你不应该试图抓住每一个例外
    每个可能的地方。

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

    至于是否可以吞下例外。这取决于你的系统。如果您的应用程序可以处理故障情况,并且通知用户失败的原因没有任何好处,那么请继续,尽管我强烈建议您记录失败。我总是觉得很难打电话来帮助解决问题并发现他们正在吞下异常(或者替换它并抛出一个新的而不设置内部异常)。

    一般来说,我使用以下规则:

  • 在我的组件和库中,如果我打算处理它或基于它做某些事情,我只会捕获异常。或者,如果我想在异常中提供其他上下文信息。
  • 我在应用程序入口点或尽可能高的级别使用常规try catch。如果异常到达此处,我只需记录它并让它失败。理想情况下,异常永远不会到达此处
  • 我发现以下代码是一种气味:

    1
    2
    3
    4
    5
    6
    7
    8
    try
    {
        //do something
    }
    catch(Exception)
    {
       throw;
    }

    像这样的代码没有任何意义,不应该包括在内。


    我想就这个主题推荐另一个好的来源。这是对C#和Java,Anders Hejlsberg和James Gosling的发明者的访谈,分别是关于Java的Checked Exception的主题。

    失败和例外

    页面底部还有很多资源。

    我倾向于同意Anders Hejlsberg和你的看法,大多数来电者只关心操作是否成功。

    Bill Venners: You mentioned
    scalability and versioning concerns
    with respect to checked exceptions.
    Could you clarify what you mean by
    those two issues?

    Anders Hejlsberg: Let's start with
    versioning, because the issues are
    pretty easy to see there. Let's say I
    create a method foo that declares it
    throws exceptions A, B, and C. In
    version two of foo, I want to add a
    bunch of features, and now foo might
    throw exception D. It is a breaking
    change for me to add D to the throws
    clause of that method, because
    existing caller of that method will
    almost certainly not handle that
    exception.

    Adding a new exception to a throws
    clause in a new version breaks client
    code. It's like adding a method to an
    interface. After you publish an
    interface, it is for all practical
    purposes immutable, because any
    implementation of it might have the
    methods that you want to add in the
    next version. So you've got to create
    a new interface instead. Similarly
    with exceptions, you would either have
    to create a whole new method called
    foo2 that throws more exceptions, or
    you would have to catch exception D in
    the new foo, and transform the D into
    an A, B, or C.

    Bill Venners: But aren't you breaking
    their code in that case anyway, even
    in a language without checked
    exceptions? If the new version of foo
    is going to throw a new exception that
    clients should think about handling,
    isn't their code broken just by the
    fact that they didn't expect that
    exception when they wrote the code?

    Anders Hejlsberg: No, because in a lot
    of cases, people don't care. They're
    not going to handle any of these
    exceptions. There's a bottom level
    exception handler around their message
    loop. That handler is just going to
    bring up a dialog that says what went
    wrong and continue. The programmers
    protect their code by writing try
    finally's everywhere, so they'll back
    out correctly if an exception occurs,
    but they're not actually interested in
    handling the exceptions.

    The throws clause, at least the way
    it's implemented in Java, doesn't
    necessarily force you to handle the
    exceptions, but if you don't handle
    them, it forces you to acknowledge
    precisely which exceptions might pass
    through. It requires you to either
    catch declared exceptions or put them
    in your own throws clause. To work
    around this requirement, people do
    ridiculous things. For example, they
    decorate every method with,"throws
    Exception." That just completely
    defeats the feature, and you just made
    the programmer write more gobbledy
    gunk. That doesn't help anybody.

    编辑:添加了有关转换的更多详细信息


    检查异常通常是一个有争议的问题,特别是在Java中(稍后我将尝试为那些赞成并反对它们的人找到一些例子)。

    根据经验,异常处理应该围绕这些准则,而不是特定的顺序:

    • 为了可维护性,请始终记录异常,以便在您开始看到错误时,日志将帮助您指向错误可能已启动的位置。永远不要离开printStackTrace()或类似的东西,很可能你的一个用户最终会得到其中一个堆栈跟踪,并且对于如何处理它几乎一无所知。
    • 捕获可以处理的异常,只有那些并处理它们,不要只是将它们抛到堆栈中。
    • 总是捕获一个特定的异常类,通常你永远不应该捕获类型Exception,你很可能会吞下其他重要的异常。
    • 从不(永远)捕获Error s !!,意思是:永远不要捕获Throwable,因为Error是后者的子类。 Error是你很可能永远无法处理的问题(例如OutOfMemory或其他JVM问题)

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


    您应该只捕获可以处理的异常。例如,如果您正在处理通过网络阅读并且连接超时而您获得异常,则可以再试一次。但是,如果您正在通过网络阅读并获得IndexOutOfBounds异常,那么您实际上无法处理它,因为您没有(好吧,在这种情况下您不会)知道是什么导致它。如果您要返回false或-1或null,请确保它是针对特定异常的。当抛出的异常是堆内存不足时,我不希望我正在使用的库在网络读取时返回false。


    例外是不属于正常程序执行的错误。根据您的程序及其用途(即文字处理器与心脏监视器)的不同,您可能希望在遇到异常时执行不同的操作。我使用的代码使用异常作为正常执行的一部分,它绝对是代码味道。

    防爆。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    try
    {
       sendMessage();

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

    这段代码让我成了barf。 IMO你不应该从例外中恢复,除非它是一个关键程序。如果你抛出异常,那么坏事就会发生。


    以上所有内容似乎都是合理的,通常您的工作场所可能有政策。在我们的位置,我们已经定义了Exception的类型:SystemException(未选中)和ApplicationException(已选中)。

    我们已经同意SystemException s不太可能被恢复,并且将在顶部处理一次。为了提供进一步的上下文,我们的SystemException被扩展以指示它们发生的位置,例如RepositoryExceptionServiceEception

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

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


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

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

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

    C方式将是

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

    ...返回值是成功指标。但你不再在C世界了。

    一旦你接受了异常,你应该通过将错误处理代码从核心逻辑中移开来发现它使代码更简单,更清晰。目标是在单个try块中包含大量语句。

    正如其他人所指出的那样 - 你应该尽可能具体地捕捉你遇到的异常。


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

    在为诊断目的记录异常时,还要考虑使用异常过滤器。 VB具有Exception过滤器的语言支持。 Greggm博客的链接有一个可以在C#中使用的实现。异常过滤器具有比catch和rethrow更好的可调试性。具体来说,您可以在过滤器中记录问题并让异常继续传播。该方法允许附加JIT(即时)调试器以具有完整的原始堆栈。重新抛出时重新抛出堆栈。

    TryXXXX有意义的情况是当你包装第三方函数时抛出非真正异常的情况,或者在不调用函数的情况下很难测试。一个例子是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    // 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这样的模式更像是一个风格问题。捕捉所有异常并吞下它们的问题不是风格问题。确保允许传播意外的异常!


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

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

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


    我的答案可能有点晚,但错误处理是我们可以随时改变和发展的事情。如果你想阅读更多关于这个主题的内容,我在我的新博客中写了一篇关于它的文章。 http://taoofdevelopment.wordpress.com

    快乐的编码。


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

    尽管如此,他们合理地使用了可读性的奇迹,但你应该遵循两个简单的规则:

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

    • 在较高级别使用一个大型处理程序来管理代码中出现的任何或所有奇怪的条件,这些条件不是在低级别捕获的。做一些有用的错误(日志,重启,恢复等)。

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

    保罗。


    这里有一些很好的答案我想补充一点,如果你最终得到像你发布的东西,至少打印超过堆栈跟踪。说出你当时在做什么,以及Ex.getMessage(),给开发者一个战斗机会。


    我的策略:

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

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

    可以返回包含错误描述的字符串而不是bool。

    在每种情况下,在返回之前记录错误。


    如果您要捕获异常并返回false,则应该是一个非常特殊的异常。你没有这样做,你抓住所有这些并返回虚假。如果我得到一个MyCarIsOnFireException,我想马上知道它!我可能不关心的其他例外情况。因此,对于某些异常(重新抛出,或者捕获并重新抛出一个更好地解释发生了什么的新异常)并且只为其他人返回false,你应该有一堆Exception处理程序,这些异常处理程序会说"whoa whoa some wrong"。

    如果这是您要发布的产品,那么您应该在某处记录这些异常,这将有助于您将来调整。

    编辑:关于在try / catch中包装所有内容的问题,我认为答案是肯定的。代码中的异常应该是如此罕见,以至于catch块中的代码很少执行,根本不会达到性能。例外应该是您的状态机损坏并且不知道该怎么做的状态。至少重新抛出一个异常,解释当时发生的事情,并在其中发现捕获的异常。"方法doSomeStuff()中的异常"对于那些在度假(或新工作)时必须弄清楚它为何会破坏的人来说并不是很有帮助。