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

我无法决定如何在应用程序中处理异常。

如果我的异常问题来自于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/catch 或不给予异常它的尊重而导致的代码腐烂(一个异常是警告系统,还需要警告什么?).

而且,这是来自 M3rLinEz的一个很好的选择注释。

我倾向于同意安德斯·海尔斯伯格和你的观点 小心手术是否成功。

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

  • 引发此异常的目的是什么?
  • 处理这件事有意义吗?
  • 调用者真的关心异常吗? 还是仅仅关心调用是否成功?
  • 强制调用方管理潜在的异常是否合适?
  • 你尊重这种语言的习语吗?
    • Do you really need to return a success flag like boolean? Returning boolean (or an int) is more of a C mindset than a Java (in Java you would just handle the exception) one.
    • Follow the error management constructs associated with the language :) !
33236 次浏览

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

至于你的问题:

  1. 您应该只捕获实际上可以处理的异常 在大多数情况下,捕捉异常并不是正确的做法。 有一些异常(例如,日志记录和编组异常) 线程之间) ,但即使在这些情况下,通常也应该 重新引发异常。
  2. 您绝对不应该在您的 同样,其思想是只捕获您可以处理的异常。 您可以包含一个最顶端的异常处理程序来转换任何未处理的 异常转换成对最终用户有用的东西,但是 否则,您不应该尝试捕获 所有可能的地方。

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

至于是否可以接受异常。这取决于你的系统。如果您的应用程序可以处理失败的情况,并且通知用户它为什么失败没有任何好处,那么请继续,尽管我强烈建议您记录失败。我总是发现,调用它来帮助解决问题,却发现它们正在吞噬异常(或者替换它并抛出一个新的异常,而不设置内部异常) ,这种做法令人沮丧。

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

  1. In my components & libraries I only catch an exception if I intend to handle it or do something based on it. Or if I want to provide additional contextual information in an exception.
  2. 我在应用程序入口点或可能的最高级别使用一般的 try catch。如果一个异常到达这里,我只是记录它,让它失败。理想情况下,异常永远不应该出现在这里。

我发现以下代码是一种气味:

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

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

如果要捕获一个 Exception 并返回 false,那么它应该是一个非常特定的异常。你不能这么做,你要把他们都抓住,然后假装回来。如果我得到一个 MyCarIsOnFireException 异常,我想马上知道它!其他的例外我可能不在乎。因此,对于某些异常(重新抛出,或者捕获并重新抛出一个能够更好地解释所发生的情况的新异常) ,您应该有一堆异常处理程序,它们会说“嚯嚯,这里出错了”,而对于其他异常,它们只返回 false。

如果这是您将要发布的产品,那么您应该将这些异常记录到某个地方,它将帮助您在将来调整这些事情。

编辑: 关于用 try/catch 包装所有东西的问题,我认为答案是肯定的。异常在您的代码中应该非常罕见,以至于 catch 块中的代码执行得非常少,以至于根本不会影响性能。异常应该是状态机中断并且不知道该做什么的状态。至少重新引发一个异常,该异常解释当时正在发生的情况,并在其中包含捕获的异常。“ Exception in method doSome Stuff ()”对于那些必须弄清楚为什么在度假时(或在新工作中)它会坏掉的人来说没有多大帮助。

After some thought and looking at your code it seems to me that you are simply rethrowing the exception as a boolean. You could just let the method pass this exception through (you don't even have to catch it) and deal with it in the caller, since that's the place where it matters. If the exception will cause the caller to retry this function, the caller should be the one catching the exception.

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

另一方面,如果异常表明程序中存在不可恢复的错误(例如,这个异常的最终结果将是程序终止) ,我个人喜欢通过捕捉并抛出运行时异常来明确这一点。

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

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

As rules of thumb, exception handling should be something around these guidelines, in no particular order:

  • 出于可维护性的考虑,始终记录异常,这样当您开始看到 bug 时,日志将帮助您指向 bug 可能已经开始的位置。永远不要离开 printStackTrace()或者类似的东西,很有可能你的某个用户最终会得到其中的一个栈跟踪,并且让 一无所知知道如何处理它。
  • 捕获您可以处理的异常,并且只有那些异常,即 处理他们,不要将它们扔到堆栈中。
  • Always catch a specific exception class, and generally you should never catch type Exception, you are very likely to swallow otherwise important exceptions.
  • Never (ever) catch Errors!!, meaning: 从来没有抓到 Throwable as Errors are subclasses of the latter. Errors are problems you will most likely never be able to handle (e.g. OutOfMemory, or other JVM issues)

关于您的特定情况,请确保调用您的方法的任何客户端都将收到正确的返回值。如果某个方法失败,返回布尔值的方法可能返回 false,但要确保调用该方法的位置能够处理这个问题。

异常是不属于正常程序执行的错误。根据程序的功能和用途(比如文字处理器和心脏监视器) ,当遇到异常时,需要执行不同的操作。我曾经处理过使用异常作为正常执行的一部分的代码,它肯定是一种代码味道。

Ex.

try
{
sendMessage();


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

这个密码让我想吐。IMO 你不应该从异常中恢复,除非它是一个关键的程序。如果抛出异常,那么坏事情就会发生。

All of the above seems reasonable, and often your workplace may have a policy. At our place we have defined to types of Exception: SystemException (unchecked) and ApplicationException (checked).

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

ApplicationException可以像 InsufficientFundsException一样具有业务意义,并且应该由客户机代码处理。

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

My strategy:

如果原来的函数返回 void,我将它改为返回 布尔。如果发生异常/错误,返回 false,如果一切正常,返回 没错

如果函数应该返回一些内容,那么当异常/错误发生时,返回 无效,否则返回可返回的项。

返回一个包含错误描述的 绳子,而不是 布尔

In every case before returning anything log the error.

我想推荐关于这个话题的另一个好的资料来源。这是一个关于 Java 的检查异常的采访,采访的对象是 C # 和 Java 的发明者,分别是安德斯·海尔斯伯格和 James Gosling。

Failure and Exceptions

There are also great resources at the bottom of the page.

我倾向于同意安德斯·海尔斯伯格和你的观点,即大多数来电者只关心手术是否成功。

比尔 · 维纳斯 : 你提到 scalability and versioning concerns 关于检查的异常。 你能澄清一下你所说的 those two issues?

安德斯·海尔斯伯格 : 让我们从 版本控制,因为问题是 很容易看出来,比如说我 创建一个方法 foo 来声明它 引发异常 A、 B 和 C version two of foo, I want to add a bunch of features, and now foo might 抛出异常 D。这是一个中断 改变我加上 D 的投掷 子句,因为 该方法的现有调用方将 几乎肯定处理不了 例外。

Adding a new exception to a throws 新版本中的子句中断客户端 类似于向 发布一个 界面,它是为所有实际 purposes immutable, because any 实施它可能会有 方法中添加的 下一个版本。所以你必须创建 a new interface instead. Similarly with exceptions, you would either have 创建一个全新的方法 抛出更多异常的 foo2,或者 you would have to catch exception D in 把 D 转换成 A、 B 或 C。

比尔 · 维纳斯 : 但是你不是在打破 their code in that case anyway, even 用一种未经检查的语言 例外? 如果新版本的 foo 将抛出一个新的异常 客户应该考虑处理, 他们的密码不就是被 事实上,他们没有预料到这一点 他们写代码的时候有什么异常吗?

安德斯·海尔斯伯格 : 不,因为在很多 人们不在乎,他们 我不会处理这些的 例外,这是最底层的 异常处理程序 loop. That handler is just going to 提出一个对话,说明发生了什么 错误并继续。程序员 通过编写 try 来保护他们的代码 终于到处都是了,所以他们会回来的 out correctly if an exception occurs, 但实际上他们并不感兴趣 处理异常。

抛出条款,至少方式 it's implemented in Java, doesn't 必然会迫使你处理 例外,但如果你不处理 them, it forces you to acknowledge 确切地说哪些例外可能通过 通过。它需要你要么 捕获声明的异常或将它们 在你自己的抛出条款。工作 围绕着这个要求,人们做到了 可笑的事情。例如,他们 每个方法都用“掷”来装饰 “例外”这完全是 而你刚刚做了一个 程序员写的东西更冗长 黏糊糊的,对谁都没好处。

编辑: 添加了更多有关对话的细节

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

为诊断目的记录异常时也使用 考虑使用 < a href = “ http://blogs.msdn.com/Greggm/archive/2008/04/21/eption-Filter-inject.aspx”rel = “ nofollow noReferrer”> Exception Filter 。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 这样的模式更多的是一个样式问题。抓住所有例外并接受它们的问题不是一个风格问题。确保允许传播意外的异常!

这里有一些很棒的答案。我想要补充的是,如果您最终得到的东西与您发布的一样,至少打印超过堆栈跟踪。告诉开发人员当时正在做什么,以及 Ex.getMessage () ,以便给开发人员一个反击的机会。

Try/catch 块形成了嵌入在第一个(主)集合上的第二个逻辑集合,因此它们是剔除不可读、难以调试的意大利面条式代码的好方法。

尽管如此,合理使用它们在可读性方面仍然有奇效,但你只需要遵循两条简单的规则:

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

  • 在较高级别使用一个大的处理程序来管理代码中出现的任何或所有在低级别没有捕捉到的怪异条件。对错误(日志、重新启动、恢复等)执行一些有用的操作。

除了这两种类型的错误处理之外,中间的所有其余代码都应该是自由的,没有 try/catch 代码和错误对象。这样,无论您在哪里使用它,或者您使用它做什么,它都可以按照预期的那样简单地工作。

保罗。

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

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

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

The C way would be

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

回报是成功的指标,但你已经不在 C 世界了。

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

正如其他人指出的那样——您应该尽可能具体地处理所捕获的异常类型。

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

编程愉快。