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

我的异常问题很大程度上来自 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.

我倾向于同意安德斯·海尔斯伯格(Anders Hejlsberg)和您的观点,即只有最多的来电者 注意手术是否成功。

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

  • 抛出这个异常有什么意义?
  • 怎样处理才有意义呢?
  • 调用者是否真的关心异常,还是只关心调用是否成功?
  • 强制调用者管理潜在的异常是否优雅?
  • 您尊重该语言的习惯吗?
    • 你真的需要返回一个像布尔值这样的成功标志吗?返回 boolean(或 int)更像是一种 C 思维方式,而不是 Java 思维方式(在 Java 中你只需处理异常)。
    • 遵循与该语言相关的错误管理结构:)!
有帮助吗?

解决方案

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

至于你的问题:

  1. 您应该只捕获您实际可以处理的异常。只 在大多数情况下,捕获异常并不是正确的做法。有一些例外(例如日志记录和封送异常 线程之间),但即使对于这些情况,您通常也应该这样做 重新引发异常。
  2. 你绝对不应该在你的 try/catch 语句中有很多 try/catch 语句 法典。同样,我们的想法是只捕获您可以处理的异常。您可以包含最顶层的异常处理程序,以将任何未处理的异常处理程序 异常对最终用户有用,但 否则,您不应尝试捕获 每一个可能的地方。

其他提示

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

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

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

  1. 在我的组件和库中,如果我打算处理它或基于它做一些事情,我只会捕获异常。或者如果我想在异常中提供额外的上下文信息。
  2. 我在应用程序入口点或尽可能的最高级别使用一般的 try catch。如果出现异常,我只是将其记录下来并让它失败。理想情况下,异常永远不应该出现在这里。

我发现下面的代码有味道:

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

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

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

失败和异常

页面底部还有很棒的资源。

我倾向于同意 Anders Hejlsberg 和您的观点,即大多数呼叫者只关心操作是否成功。

比尔·文纳斯: :你提到 可伸缩性和版本控制问题 关于已检查的例外情况。你能澄清一下你的意思吗 这两个问题?

安德斯·海尔斯伯格: :让我们从以下方面开始 版本控制,因为问题是 那里很容易看到。比方说我 创建一个声明它的方法 foo 引发异常 A、B 和 C。在 foo 的第二个版本,我想添加一个 一堆功能,现在 Foo 可能 引发异常 D。这是一个突破 为我更改以将 D 添加到投掷中 子句,因为 该方法的现有调用方将 几乎可以肯定不会处理这个问题 例外。

向抛出添加新异常 新版本中的子句会中断客户端 法典。这就像将方法添加到 接口。发布 接口,适合所有实用 目的不可变,因为任何 它的实现可能具有 要添加到 下一个版本。所以你必须创造 取而代之的是一个新界面。同样地 除例外情况外,您将拥有 创建一个全新的方法,称为 foo2 引发更多异常,或者 您必须在 新的 foo,并将 D 转换为 A、B 或 C。

比尔·文纳斯: :但你不是坏了吗 无论如何,在这种情况下,他们的代码,甚至 使用未选中的语言 异常?如果新版本的 foo 将抛出一个新的异常 客户应该考虑处理, 他们的代码不就是被 事实上,他们没想到 他们编写代码时出现异常?

安德斯·海尔斯伯格: :不,因为在很多 的案例,人们不在乎。他们是 不会处理其中任何一个 异常。有一个底层 消息周围的异常处理程序 圈。该处理程序只是要 弹出一个对话框,说明发生了什么 错误并继续。程序员 通过编写 try 来保护他们的代码 终于无处不在了,所以他们会回来的 如果发生异常,则正确输出, 但他们实际上并不感兴趣 处理异常。

throws 子句,至少是这样 它是用 Java 实现的,不是 必然迫使你处理 异常,但如果您不处理 他们,它迫使你承认 确切地说,哪些异常可能会通过 通过。它要求您 捕获声明的异常或放置它们 在你自己的 throws 子句中。工作 围绕这个要求,人们会这样做 荒谬的事情。例如,他们 用“投掷”装饰每种方法 例外。这完全是 击败了该功能,而您刚刚制作了 程序员写得更狼吞虎咽 咕噜咕噜。这对任何人都没有帮助。

编辑:添加了有关对话的更多详细信息

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

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

  • 为了可维护性,请始终记录异常,以便当您开始看到错误时,日志将帮助您指出错误可能开始的位置。永远不离开 printStackTrace() 或类似的情况,您的用户之一最终可能会获得这些堆栈跟踪之一,并且有 完全零知识 至于如何处理它。
  • 捕获您可以处理的异常,并且仅捕获那些异常, 并处理它们, ,不要只是将它们扔到堆栈上。
  • 始终捕获特定的异常类,并且通常您不应该捕获类型 Exception, ,你很可能会吞下其他重要的异常。
  • 永远不会(永远)抓住 Error是!!, , 意义: 永远不会抓住 Throwables 作为 Errors 是后者的子类。 Error这些问题你很可能永远无法处理(例如 OutOfMemory, ,或其他 JVM 问题)

根据您的具体情况,请确保调用您的方法的任何客户端都将收到正确的返回值。如果出现故障,返回布尔值的方法可能会返回 false,但请确保调用该方法的地方能够处理该情况。

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

异常是不属于正常程序执行的错误。取决于您的程序的用途及其用途(即文字处理器 vs 文字处理器心脏监护仪)当遇到异常时,您会想要做不同的事情。我曾经使用过使用异常作为正常执行一部分的代码,这绝对是一种代码味道。

前任。

try
{
   sendMessage();

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

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

上述所有内容似乎都很合理,而且您的工作场所通常可能有一项政策。在我们这里,我们定义了异常类型: SystemException (未选中)和 ApplicationException (已检查)。

我们同意 SystemException不太可能恢复,将在顶部处理一次。为了提供进一步的背景信息,我们的 SystemExceptions 被扩展以指示它们发生的位置,例如 RepositoryException, ServiceEception, , ETC。

ApplicationExceptions 可能具有商业意义,例如 InsufficientFundsException 并且应该由客户端代码处理。

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

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

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

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

如果您打算在示例中使用该代码模式,请将其命名为 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 这样的模式更多的是一个风格问题。捕获所有异常并吞掉它们的问题不是风格问题。确保允许意外异常传播!

我建议从您正在使用的语言的标准库中获取线索。我不能谈论 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 堵塞。

正如其他人指出的那样 - 您应该尽可能具体地描述您捕获的异常类型。

如果您要捕获异常并返回 false,那么它应该是一个非常具体的异常。你没有这样做,你捕获了所有这些并返回 false。如果我收到 MyCarIsOnFireException 我想立即了解它!其余的例外我可能不关心。因此,您应该有一堆异常处理程序,对于某些异常(重新抛出,或捕获并重新抛出一个新的异常,以更好地解释发生的情况)说“哇哦,这里出了问题”,而对于其他异常则返回 false。

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

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

我的策略:

如果原来的函数返回 空白 我将其更改为返回 布尔值. 。如果发生异常/错误则返回 错误的, ,如果一切正常返回 真的.

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

代替 布尔值 A 细绳 可以返回包含错误描述的信息。

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

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

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

尽管如此,合理使用它们在可读性方面会产生奇迹,但您应该遵循两个简单的规则:

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

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

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

保罗.

我的答案可能有点晚了,但错误处理是我们总是可以随着时间的推移而改变和发展的。如果您想了解有关该主题的更多信息,我在我的新博客中写了一篇关于它的文章。 http://taoofdevelopment.wordpress.com

快乐编码。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top