【问题标题】:Is it ok to catch all exception types if you rethrow them wrapped another exception?如果重新抛出它们包装了另一个异常,是否可以捕获所有异常类型?
【发布时间】:2010-02-25 20:12:12
【问题描述】:

我知道您不应该编写像这样缓存所有异常类型的代码。

try
{
  //code that can throw an exception
}
catch
{
   //what? I don't see no
}

相反,您应该做一些更像下面的代码的事情,允许任何其他您没想到会冒泡的异常。

try
{
//code that can throw an exception
}
catch(TypeAException)
{
   //TypeA specific code
}

catch(TypeBException)
{
   //TypeB specific code
}

但是,如果您用另一个异常包装它们,是否可以捕获所有异常类型? 考虑下面这个Save() 方法,我正在编写作为目录类的一部分。我捕获所有异常类型并返回单个自定义 CatalogIOException 并将原始异常作为内部异常有什么问题吗?

基本上,我不希望任何调用代码必须知道有关 Save() 方法内部可能引发的所有特定异常的任何信息。他们只需要知道他们是否尝试保存只读目录 (CatalogReadOnlyException)、无法序列化目录 (CatalogSerializationException) 或写入文件是否有问题 (CatalogIOException)。

这是处理异常的好方法还是坏方法?

/// <summary>
/// Saves the catalog
/// </summary>
/// <exception cref="CatalogReadOnlyException"></exception>
/// <exception cref="CatalogIOException"></exception>
/// <exception cref="CatalogSerializingExeption"></exception>
public void Save()
{
    if (!this.ReadOnly)
    {
        try
        {
            System.Xml.Serialization.XmlSerializer serializer = new XmlSerializer(typeof(Catalog));
            this._catfileStream.SetLength(0); //clears the file stream
            serializer.Serialize(this._catfileStream, this);
        }
        catch (InvalidOperationException exp)
        {
            throw new CatalogSerializationException("There was a problem serializing the catalog", exp);
        }
        catch (Exception exp)
        {
            throw new CatalogIOException("There was a problem accessing the catalog file", exp);
        }
    }
    else
    {
        throw new CatalogReadOnlyException();
    }
}

更新 1

感谢您迄今为止的所有回复。听起来共识是我不应该这样做,而且我应该只在我确实与它们有关的情况下捕捉异常。在这个 Save() 方法的情况下,确实没有任何我想在 Save() 方法本身中处理的异常。大多数情况下,我只是想通知用户他们无法保存的原因。

我认为我真正的问题是我使用异常作为通知用户问题的一种方式,我让这告诉我如何创建和处理异常有点过多。因此,听起来最好不要捕获任何异常并让 UI 层弄清楚如何通知用户,或者崩溃。这个对吗?考虑下面的 Save Menu 事件处理程序。

    private void saveCatalogToolStripMenuItem_Click(object sender, EventArgs e)
    {
        //Check if the catalog is read only
        if (this.Catalog.ReadOnly)
        {
            MessageBox.Show("The currently opened catalog is readonly and can not be saved");
            return;
        }

        //attempts to save
        try
        {
            //Save method doesn't catch anything it can't deal with directly
            this.Catalog.Save(); 
        }
        catch (System.IO.FileNotFoundException)
        {
            MessageBox.Show("The catalog file could not be found");
        }
        catch (InvalidOperationException exp)
        {
            MessageBox.Show("There was a problem serializing the catalog for saving: " + exp.Message);
        }
        catch (System.IO.IOException exp)
        {
            MessageBox.Show("There was a problem accessing the catalog file: " + exp.Message);
        }
        catch (Exception exp)
        {
            MessageBox.Show("There was a problem saving the catalog:" + exp.Message);
        }
    }

更新 2

还有一件事。如果 Save() 方法是公共 API 与内部代码的一部分,答案会完全改变吗?例如,如果它是公共 API 的一部分,那么我必须找出并记录 Save() 可能抛出的所有可能异常。如果知道 Save() 只能抛出我的三个自定义异常之一,这将容易得多。

此外,如果 Save() 是公共 API 的一部分,安全性是否也不是问题?也许我想让 API 的使用者知道保存不成功,但我不想通过让他们获取可能已生成的异常来公开有关 Save() 工作原理的任何信息。

【问题讨论】:

  • 使用您的更新是一种同样有效的方法来处理它,但是您已将消费者转移到比您的原始模型负责更多潜在错误案例,尽管在这种情况下只有 1 个更具体。
  • 我认为大多数论点都反对你的最后一项 catch(Exception) 而不是包装其余的。因此,从这个意义上说,您会将 FileNotFound、InvalidOp 等异常保留在较低层(并包装)。建议只是不应该包括您的最终捕获(异常),因为它确实是一个异常,而您已经计划了其他可能性。
  • @Eric - 更新了我的回复 - 看看你的想法。

标签: c# .net exception-handling


【解决方案1】:

做一个通用的包罗万象并作为一种新的异常类型重新抛出并不能真正解决你的问题,也不会给你任何东西。

您真正需要做的是捕获您可以处理的异常,然后处理它们(在适当的级别 - 这是重新抛出可能有用的地方)。所有其他异常都需要记录,以便您可以调试它们发生的原因,或者首先不应该发生(例如 - 确保您验证用户输入等)。如果您捕获所有异常,您将永远不会真正知道为什么会收到异常,因此无法修复它们。

更新回复

针对您的问题的更新(特别是您希望如何处理保存案例),我向您提出的问题是 - 为什么要使用异常来确定程序所采用的路径?例如,让我们以“FileNotFoundException”为例。显然,这有时会发生。但是,与其让问题发生并通知用户,在保存(或做任何事情)文件之前,为什么不先检查文件是否可以找到。您仍然可以获得相同的效果,但您没有使用异常来控制程序流程。

我希望这一切都有意义。如果您有任何其他问题,请告诉我。

【讨论】:

  • 你是说返回错误码而不是抛出异常?
  • 是的,我也意识到了这一点。最好进行预先验证,而不是等待事情失败然后处理它们。
  • @dan - 不一定。我是说,不要依赖异常来控制程序流程。 Eric 可以通过先进行检查,向用户抛出通知,然后返回,来实现与上面的代码相同的效果。这样,他就不会产生异常的开销,并且更容易调试和测试。
  • 检查文件是否存在并不能保证在您阅读它时它不会消失,即使它在下一行代码中也是如此。检查可以让您在大多数情况下避免不必要的异常,但您仍然需要进行错误处理以防万一。
  • @Rory - 很好,我后来意识到了这一点,但不想将我的答案更改为其他内容(例如安全权限)。
【解决方案2】:

当您将原始异常作为内部异常重新抛出时,您会丢失原始堆栈跟踪,这是有价值的调试信息。

我有时会按照你的建议去做,但我总是先记录原始异常以保留堆栈跟踪。

【讨论】:

  • 既然他包装了原始异常,它如何丢失它的堆栈轨道?它应该作为他的基础例外提供。
  • 确实,'throw ex'和'throw'的区别
  • tkachenko.com/blog/archives/000352.html 但是为了平衡,这里有一个注释可以进一步讨论:weblogs.asp.net/fmarguerie/archive/2008/01/02/…
  • @Gabe: InnerException 容易忘记?如果您正确使用ex.ToString() 来显示异常,则不会。
  • 使用过 WPF 或 BackgroundWorker 的人永远不会忘记内部异常。
【解决方案3】:

我认为您的操作没有问题。将异常包装在自定义异常类型中的原因是在代码层之间创建抽象——将较低级别的错误转换为较高级别的上下文。这样做可以让调用代码不必过多了解 Save 所做的实现细节。

您的update #1 是调用代码必须对Save() 的实现细节了解太多的示例。对于您的第二次更新,我同意 100%

PS
我并不是说在遇到异常的每种情况下都这样做。只有当收益大于成本时(通常在模块边界)。

当这特别有用时的示例场景:您正在包装一个 3rd 方库,您还不知道可能引发的所有底层异常,您没有源代码或任何文档,等等.

此外,他正在包装底层异常并且不会丢失任何信息。仍然可以适当地记录异常(尽管您需要通过InnerExceptions 递归)。

【讨论】:

    【解决方案4】:

    我赞成包装异常,因为自定义异常层次结构可以将异常划分为比默认层次结构更有用的分类。假设一个人试图打开一个文档并得到一个 ArgumentException 或一个 InvalidOperationException。异常的类型是否真的包含任何有用的信息?但是,假设您得到了 CodecNotFoundException、PrerenderFingFailureException 或 FontLoadFailureException。可以想象系统捕获了其中的一些异常并尝试对其进行处理(例如,允许用户搜索编解码器,使用较低分辨率设置重试渲染,或者在警告用户后允许替换字体)。比默认异常有用得多,其中许多都没有说明真正的错误或可以采取的措施。

    从分层的角度来看,真正需要的是一种区分异常的方法,表明抛出异常的方法无法执行其任务,但系统状态与方法启动之前的状态相似,并且那些这表明系统状态以超出方法失败所暗示的方式损坏。正常的异常层次结构对此完全没有用;如果包含异常,则可能会稍微改善这种情况(尽管不如层次结构设计得更好)。强制解除事务的异常并不像提交或解除事务时发生的异常那么糟糕。在前一种情况下,系统的状态是已知的;在后一种情况下,它不是。

    虽然应该避免捕获某些非常糟糕的异常(StackOverflowException、OutOfMemoryException、ThreadAbortException),但我不确定它是否真的很重要。如果系统要崩溃和烧毁,无论是否捕获到异常,它都会这样做。在 vb.net 中,“Catch Ex As Exception When IsNotHorribleException(Ex)”可能是值得的,但 C# 没有这样的结构,甚至没有办法排除某些异常被捕获。

    分页说明:在某些情况下,一个操作可能会产生多个值得记录的异常。只有将异常包装在包含其他异常列表的自定义异常中才能真正实现。

    【讨论】:

      【解决方案5】:

      我认为这不是一个好主意。

      如果您有任何要添加的内容,您应该只添加您自己的异常类型。

      此外,您应该只捕获您期望并且您能够处理的异常 - 应该允许所有其他异常冒泡。

      作为一名开发人员,我必须说,如果你试图通过吞下或包装异常来“隐藏”我的异常,我会很生气。

      【讨论】:

      • 他并没有隐藏异常,众所周知,这种方法永远只会抛出这 3 个异常。消费者有责任以任何所需的方式处理这 3 个异常。我认为这里没有问题。
      【解决方案6】:

      有关为什么 catch(exception) 不好的更多信息,请查看这篇文章:http://blogs.msdn.com/clrteam/archive/2009/02/19/why-catch-exception-empty-catch-is-bad.aspx

      基本上捕捉“异常”就像说“如果出现任何问题,我不在乎继续”,捕捉“异常”并包装它就像是在说“如果出现任何问题,将它们视为完全相同的问题对待它们原因”。

      这不可能是正确的,要么你处理它是因为你半预期它,要么你完全不认为它应该永远发生(或不知道它会发生)。在这种情况下,您需要某种应用级别的日志记录来指出您从未预料到的问题 - 而不仅仅是一种万能的解决方案。

      【讨论】:

      • 阅读关于您发布的文章的评论 #3,它甚至指出 Catch(Exception) 有其用途。
      【解决方案7】:

      我自己的经验法则是捕获并包装Exception,前提是我可以添加一些有用的上下文,例如我尝试访问的文件名或连接字符串等。如果弹出InvalidOperationException在你的 UI 中没有附加任何其他信息,你将有一个地狱般的时间来追踪错误。

      只有当我想添加的上下文或消息比我对Exception 通常所说的更有用时,我才会捕获特定的异常类型。

      否则,我会让异常冒泡到另一个可能需要添加一些有用的方法的方法。我不想做的就是让自己陷入困境,试图捕捉、声明和处理所有可能的异常类型,尤其是因为你永远不知道运行时什么时候可能会抛出一个偷偷摸摸的 ThreadAbortExceptionOutOfMemoryException

      所以,在你的例子中,我会做这样的事情:

      try
      {
          System.Xml.Serialization.XmlSerializer serializer = 
              new XmlSerializer(typeof(Catalog));
          this._catfileStream.SetLength(0); //clears the file stream
          serializer.Serialize(this._catfileStream, this);
      }
      // catch (InvalidOperationException exp)
      // Don't catch this because I have nothing specific to add that 
      // I wouldn't also say for all exceptions.
      catch (Exception exp)
      {
          throw new CatalogIOException(
              string.Format("There was a problem accessing catalog file '{0}'. ({1})",
                  _catfileStream.Name, exp.Message), exp);
      }
      

      考虑将内部异常的消息添加到您的包装异常中,这样如果用户只是向您发送错误对话框的屏幕截图,您至少拥有所有消息,而不仅仅是顶部的消息;如果可以的话,将整个ex.ToString() 写入某个日志文件。

      【讨论】:

        【解决方案8】:

        在这种特殊情况下,异常应该很少见,以至于包装它不应该是一件有用的事情,并且可能只会妨碍错误处理。 .Net 框架中有很多示例,其中我可以处理的特定异常包含在更通用的异常中,这使我处理特定情况变得更加困难(尽管并非不可能)。

        【讨论】:

          【解决方案9】:

          我以前写过一篇关于这个主题的文章。在其中,我重申尽可能多地捕获有关异常的数据的重要性。这是文章的网址:

          http://it.toolbox.com/blogs/paytonbyrd/improve-exception-handling-with-reflection-and-generics-8718

          【讨论】:

            【解决方案10】:

            用户被告知“序列化目录时出现问题”有什么好处?我想您的问题域可能是一个特殊的案例,但是我曾经为其编程过的每一组用户在阅读该消息时都会以相同的方式响应:“程序崩溃了。关于目录的问题。”

            我并不是要对我的用户居高临下;我不是。只是一般来说,我的用户有更好的事情来处理他们的注意力,而不是浪费它为我的软件内部正在发生的事情构建一个细粒度的心理模型。有时我的用户必须建立这样的理解才能使用我编写的程序,我可以告诉你,这种体验对他们或我都没有好处。

            我认为您最好将时间花在弄清楚如何可靠地记录异常和相关的应用程序状态,以便当您的用户告诉您出现问题时您可以访问该表单,而不是想出一个用于产生错误的复杂结构人们不太可能理解的信息。

            【讨论】:

              【解决方案11】:

              要回答这个问题,您需要了解为什么捕获 System.Exception 是一个坏主意,并了解您自己执行建议的动机。

              这是一个坏主意,因为它声明任何可能出错的都是可以的,并且应用程序处于良好状态以便之后继续运行。这是一个非常大胆的声明。

              所以问题是:您提出的建议是否等同于捕获 System.Exception?它是否为您的 API 的使用者提供了更多知识以做出更好的判断?它是否只是鼓励他们捕获 YourWrappedException,这在道德上等同于捕获 System.Exception,除了不触发 FXCop 警告?

              在大多数情况下,如果您知道有一条禁止做某事的规则,并且想知道类似的事情是否“可以”,您应该从了解原始规则的基本原理开始。

              【讨论】:

              • 一个中间应用层,它允许来自内层的意外异常在不被包装的情况下通过它冒泡,将外部应用层提交给(1)在任何意外异常时无条件崩溃; (2) 至少在某些可能未解决的问题可能仍然存在的情况下尝试继续进行。即使应用程序知道导致内层异常的问题已经从内层的角度解决了,它也无法知道异常是否破坏了中间层的状态。
              【解决方案12】:

              这是 .NET 中的标准做法以及应如何处理异常,尤其是对于可从中恢复的异常。

              编辑:我真的不知道为什么我被否决了,也许我比其他人更了解作者的意图。但是我阅读代码的方式是,他将这些异常包装到他的自定义异常中,这意味着该方法的使用者有能力处理这些异常,并且处理错误处理是使用者的责任。

              DV 实际上没有留下任何形式的实际争议。我坚持我的回答,这是完全可以接受的,因为消费者应该意识到这可能引发的潜在异常并有能力处理它们,这可以通过异常的显式包装来证明。这也保留了原始异常,因此堆栈跟踪可用并且底层异常可访问。

              【讨论】:

              • 所以......你是说......你应该这样做,除非在那些情况下你不应该这样做?还是……?
              • 我是说这是一个非常好的做法,他定义了 3 个异常:CatalogSerializationException、CatalogIOException 和 CatalogReadOnlyException,调用者都应该知道它们,以便调用者选择如何处理它们。
              • @Chris Marisic - 我不是投反对票的人,但我猜你的答案被否决了,因为它没有太多意义。 Lasse 在他的评论中强调了这一点,即您的原始答案不清楚。无论哪种情况,我都同意消费者需要了解例外情况——毫无疑问。但是,再次抛出异常添加了什么?如果什么都没有,那么就没有重新抛出的好案例(只是增加了复杂性/开销/等)。
              • 因为这是一个保存方法,很可能会从 UI 调用,它需要重新抛出该异常,因此客户端有希望知道他们的工作没有正确保存。如果这里没有重新抛出,客户端将假定工作已成功完成。
              • 如果不包含 catch(Exception),UI 会知道它没有正确保存,因为异常会冒泡到任何应用程序级别的错误处理(可能会记录并重定向到干净的错误页面)。任何预期的异常都应该在较低级别被捕获和包装,以便使用代码知道可能的异常。如果您为尽可能识别的异常类型设置了级联捕获,那么您希望将其他任何事情视为大规模异常并记录下来。与未知异常相比,消费代码对封装的未知异常还能做什么?
              猜你喜欢
              • 2012-08-31
              • 2020-02-13
              • 1970-01-01
              • 2010-10-03
              • 1970-01-01
              • 1970-01-01
              • 2011-06-27
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多