【问题标题】:Try/Catch in every method of every class?在每个类的每个方法中尝试/捕获?
【发布时间】:2012-07-21 03:25:35
【问题描述】:

当我们将一堆语句包装在 try/catch 中并且其中一个问题引发异常时,在 catch 中我们无法知道哪些语句导致了异常(ex.stacktrace 显示了我们当前的方法(doit )、它的调用者、它的调用者的调用者等,但既不是 do1 也不是 do2):

function doit() {
   try {
     do1();
     do2();
     [...]
   }
   catch (Exception ex) {
     // what failed?
   }
}

通常我会包装所有语句并重新抛出,有点像:

private void do1() {
  try {
     // do whatever
  } catch(Exception e) {
     // write to my error log
     throw new Exception("do1: " + e.Message, e.InnerException);
  }
}

这会在我的日志中留下一条面包屑痕迹,并使该链可用于上游。当然,问题是我必须用这种代码来包装我编写的每个方法。

某事告诉我我对此很愚蠢。什么是正确的方法?

【问题讨论】:

  • 只要“.pdb”文件可用,您就会在堆栈跟踪中获得异常的行号。或者,当您处于方法中的逻辑点时设置某种形式的指示符,如果方法出现异常,也将其写入日志。不要把 try-catch 到处乱扔。
  • 我不明白这样做的动机。在第一个示例中,堆栈跟踪的顶部应该是 do1(),是吗?
  • 这是一个糟糕的想法,几乎每个开发人员都曾尝试过。
  • 异常的堆栈跟踪将显示异常的来源(方法和可能的行号),无论您在哪里捕获它。
  • 您可能在各个级别都在使用throw ex;。不要那么做。并停止在各个级别捕获它。抓住它,您可以以任何适当的方式处理它,无论是日志记录还是其他方式。无处可去。

标签: c# exception-handling


【解决方案1】:

好的,这很难做对,因为异常处理是一个非常敏感的话题,过去人们就如何正确处理这个问题进行了宗教战争。

首先:既不要使用空的 catch (try { ... } catch { ... }),也不要使用 catch(Exception ex)。 异常派生类的唯一目的是为您提供有关发生的异常类型的丰富信息,以便您可以在异常处理程序中做一些有意义的事情(如果线程崩溃重新启动它,如果数据库连接失败非永久重试,然后失败,等等)。

人们倾向于在其代码的最外层使用一个包罗万象的处理程序来记录未捕获的异常,这没问题,但无论如何你应该提示用户或重新抛出异常(使用throw,不是throw ex - 对此也有大量讨论)。

基本上,您根本不会以编程方式关心异常发生的位置。 你可以处理它,或者你不能。如果你不能处理它,那么你就不会抓住它。

附录:这个“如果你能做点什么,就去做,否则你不敢碰那个异常”哲学的最重要原因是静默捕获的异常(无论是否记录)可能会导致非常难以发现的错误。仅将它们推送到日志文件可能还不够,因为在实时系统中,您可能无法获得完整注释的堆栈跟踪(包括行号和所有内容)。

附录 2:以一个需要整数输入的文本框为例。如果用户提供了一个您无法有意义地处理输入的字符串,则可能会引发转换异常,您捕获该特定异常并将文本框重置为其旧值,并可能通知用户错误输入。或者,您的程序可能会因异常而死(糟糕的设计,您可以从该异常中恢复),或者默默地继续显示错误的输入,但仍然使用旧值(糟糕的设计,程序具有误导性)。

【讨论】:

  • 我不同意 Exception ex。在系统中的某些点,您希望捕获任何异常,这样它就不会在堆栈中进一步上行。这方面的一个例子是在您的应用程序启动类中。捕获未处理的异常,记录它,向您的用户显示一条消息,然后优雅地处理而不是让应用程序崩溃。
  • 我同意如果您大部分时间不打算处理异常,请不要触碰异常。我要说的唯一方法是,如果您要公开 API,并且想要“API 内部”日志记录,但仍希望将异常抛回给消费者。为您的回答 +1。
  • 你是对的,捕捉异常可以作为程序优雅地死掉或重启的绝望的最后手段(当然,如果异常是 OutOfMemory 并且你在异常处理程序中分配了任何内存,它就会冒泡反正出去:)
【解决方案2】:

如果您真的很喜欢这样做(其他人已经说明了为什么这样做很糟糕),请使用面向方面的编程方法。这将使您的生活变得更加轻松,并减少您最终编写和维护的代码量。

看看PostSharp,它为您提供了一个框架,允许您使用将为您生成此样板错误处理的属性来装饰方法、类或命名空间。

【讨论】:

  • 感谢您的建议。这是我一直很好奇的事情,所以我需要调查一下
【解决方案3】:

@Branko 将其钉在 cmets 中:异常的堆栈跟踪显示了引发异常的位置,而不是捕获异常的位置。

@ChaosPandion 的评论 +1:这是一个非常、非常、非常糟糕的主意。

【讨论】:

  • 你是对的。我无法弄清楚我在写这篇文章时在想什么。我需要重新访问。
  • 在重新阅读我的原始帖子时,我再次理解了我的问题,并且它被正确表达了。问题是堆栈跟踪没有表明 do1() 或 do2() 是否发出了异常,这就是我在捕获它时需要知道的。因此,如果您查看,堆栈跟踪的顶部是包含 try/catch 的方法,而不是发出异常的方法。因此,我重新提出了这个问题。谢谢。
  • 如果您不重新抛出异常,原始堆栈跟踪应该从实际异常抛出位置开始,并包含整个路径。这应该包括 do1() 或 do2() ...除非异常是在收集参数来调用它们。想象一下do1(somelist.Count),其中somelist 为空。
  • 在我的 catch 中放置一个调试器断点会显示以包含 try/catch 的函数开头的堆栈跟踪。这是我的问题,我不知道是 do1() 还是 do2() 导致了异常。我不确定“收集参数以调用它们”是什么意思,但是如果我们定义了 void do1() { throw ... } 我想在堆栈中看到 do1() (可能还有其中的行)追踪
  • robrich,(由于某种原因,at-sign-name 似乎不起作用)您的评论(以及上面的 Anthony Pegram)促使我研究投掷/重新投掷的问题。我发现了这个:goo.gl/0l0OK 这可能是在伤害我。我需要查看代码以查看是否有“新”抛出正在丢弃我的堆栈...
【解决方案4】:

trycatch 在我看来可能是设计最差的现代编程机制。我们不再有能力随时处理错误;如果发生单个异常,我们必须使整个过程失败,除非我们做一些可怕的事情,比如单独尝试每个语句。不再有恢复的选择,只能尽可能优雅地失败。

到目前为止,我发现的最佳模式是将每个用户事件包装在 try/catch 中(使用一种方法,而不是每次都显式尝试)。例如:

public static class Defines
{
   public static bool TryAction(Action pAction)
   {
      try { pAction(); return true; }
      catch(Exception exception) { PostException(exception); return false; }
   }
}

...

private void DoSomething(int pValue)
{
   ...
}

private void MyControl_MyEvent(object pSender, MyEventArgs pEventArgs)
{
   Defines.TryAction(() => DoSomething(pEventArgs.Data));
}

除此之外,只需尝试编写无异常代码即可。仅当您很可能遇到异常并且想要做的不仅仅是优雅地失败时,才使用显式 trys。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-06-08
    • 2016-11-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多