我应该抓住并包装一般的例外吗?

时间:2014-02-03 15:25:23

标签: c# .net exception exception-handling

以下代码可以被视为一种良好做法吗?如果没有,为什么?

try
{
    // code that can cause various exceptions...
}
catch (Exception e)
{
    throw new MyCustomException("Custom error message", e);
}

14 个答案:

答案 0 :(得分:27)

简短回答:除非有某些原因必须,否则不要这样做。相反,捕获您可以处理它们时可以处理的特定异常,并允许所有其他异常冒泡堆栈。


TL; DR回答:这取决于您的编写内容,调用代码的内容以及您认为需要引入自定义异常类型的原因。

我认为,最重要的问题是,如果我抓住了,我的来电有什么好处

  • 不要隐藏您的来电者需要的信息
  • 不要求你的来电者跳过必要的箍圈

想象一下,您正在处理一个低级数据源;也许你正在做一些编码或反序列化。我们的系统非常好并且模块化,因此您没有太多关于您的低级数据源是什么或者如何调用代码的信息。也许您的库部署在侦听消息队列并将数据写入磁盘的服务器上。也许它在桌面上,数据来自网络并显示在屏幕上。

可能会发生许多不同的异常(DbException,IOException,RemoteException等),并且你有 no 处理它们的有用方法。

在这种情况下,在C#中,普遍接受的事情是让异常冒泡。您的呼叫者知道该怎么做:桌面客户端可以提醒用户检查他们的网络连接,该服务可以写入日志并允许新消息排队。

如果您将异常包装在您自己的MyAwesomeLibraryException中,那么您的调用者的工作就会变得更难。您的来电者现在需要: -

  • 阅读并理解您的文档
  • 在他们想要捕捉
  • 的位置引入对程序集的依赖关系
  • 编写额外代码以询问您的自定义异常
  • 他们不关心的Rethrow例外。

确保额外的努力值得花时间。不要无缘无故!

如果您的自定义异常类型的主要理由是您可以向用户提供友好的错误消息,那么您应该警惕在捕获的异常中过于非特定。我已经失去了一些次数 - 无论是作为用户还是作为工程师 - 一个过于热心的捕获声明隐藏了问题的真正原因: -

try
{
  GetDataFromNetwork("htt[://www.foo.com"); // FormatException?

  GetDataFromNetwork(uriArray[0]); // ArrayIndexOutOfBounds?

  GetDataFromNetwork(null); // ArgumentNull?
}
catch(Exception e)
{
  throw new WeClearlyKnowBetterException(
    "Hey, there's something wrong with your network!", e);
}

或另一个例子: -

try
{
  ImportDataFromDisk("C:\ThisFileDoesNotExist.bar"); // FileNotFound?

  ImportDataFromDisk("C:\BobsPrivateFiles\Foo.bar"); // UnauthorizedAccess?

  ImportDataFromDisk("C:\NotInYourFormat.baz"); // InvalidOperation?

  ImportDataFromDisk("C:\EncryptedWithWrongKey.bar"); // CryptographicException?
}
catch(Exception e)
{
  throw new NotHelpfulException(
    "Couldn't load data!", e); // So how do I *fix* it?
}

现在我们的调用者必须解开我们的自定义异常,以便告诉用户实际出错了什么。在这些情况下,我们已经要求我们的来电者做额外的工作而没有任何好处。引入包含所有异常的自定义异常类型不是本质上一个好主意。一般来说,我: -

  1. 抓住我可以提供的最具体的例外

  2. 我可以为此做点什么

  3. 否则,我只是让异常冒泡

  4. 请记住,隐藏错误的详细信息通常并不常用

  5. 这并不意味着您从不这样做!

    1. 有时Exception是您可以捕获的最具体的例外,因为您希望以相同的方式处理所有例外 - 例如Log(e); Environment.FailFast();

    2. 有时你有一个上下文来处理一个异常的例子 - 例如您刚刚尝试连接到网络资源,并且想要重试

    3. 有时,来电者的性质意味着您不能允许异常冒泡 - 例如您正在编写日志记录代码,并且您不希望日志记录失败“替换”您尝试记录的原始异常!

    4. 有时会有一些有用的额外信息,你可以给你的调用者抛出一个在调用堆栈的高处不可用的例外 - 例如在我上面的第二个例子中,我们可以catch (InvalidOperationException e)并包含我们正在使用的文件的路径。

    5. 偶尔 出错了什么并不像 那样重要。区分FooModuleExceptionBarModuleException可能有用,无论实际问题是什么 - 例如如果有一些异步,可能会阻止你有用地查询堆栈跟踪。

    6. 虽然这是一个C#问题,但值得注意的是,在其他一些语言(尤其是Java)中,您可能会被迫换行,因为已检查的异常是方法合同的一部分 - 例如if you're implementing an interface, and the interface doesn't specify that the method can throw IOException

    7. 但这是非常普遍的东西。更具体地说,根据您的情况:为什么您认为自己需要自定义异常类型?如果我们知道这一点,我们可以为您提供更好的定制建议。

答案 1 :(得分:5)

完全没问题。您不必单独捕获每种异常类型。如果要以特定方式处理异常,可以捕获特定类型的异常。如果要以相同的方式处理所有异常 - 捕获基础Exception并像处理它一样处理它。否则,每个catch块中都会有重复的代码。

答案 2 :(得分:5)

Eric Lippert撰写了一篇精彩的博文,"Vexing exceptions"。 Eric用一些通用指南回答你的问题。以下是“总结”部分的引用:

  
      
  • 不要抓住致命的例外;无论如何你无能为力,并试图让它变得更糟。
  •   
  • 修复代码,使其永远不会触发骨干异常 - 生产代码中永远不会发生“索引超出范围”异常。
  •   
  • 尽可能通过调用那些引入非异常的烦恼方法的“尝试”版本来避免烦恼的异常   情况。如果你不能避免调用一个令人烦恼的方法,赶上它   令人烦恼的例外。
  •   
  • 始终处理表示意外外生条件的异常;一般来说,预期是不值得或不实际的   每一次可能的失败试试操作并做好准备   处理异常。
  •   

答案 3 :(得分:4)

不,通常,您不应该这样做:这可能会掩盖真正的异常,这可能表示您的代码中存在编程问题。例如,如果try / catch中的代码有一个错误的行导致数组索引超出绑定错误,那么您的代码也会捕获它,并为它抛出一个自定义异常。自定义异常现在没有意义,因为它报告了编码问题,所以没有人在代码之外捕获它就能够用它做任何有意义的事情。

另一方面,如果try / catch内的代码抛出了您期望的异常,那么在自定义异常中捕获和包装它们是个好主意。例如,如果您的代码从组件的某些特殊文件读取,并且读取导致I / O异常,则捕获该异常并报告自定义异常是一个好主意,因为它可以帮助您从调用方隐藏文件操作

答案 4 :(得分:3)

这就是我通常所说的异常处理:

  • 与例外有关的第一件事是......什么都没有。将有一个高级别捕获所有处理程序(如黄色ASP.NET错误页面)将帮助您,并且您将有一个完整的堆栈框架(请注意,在其他非.NET环境中不是这种情况)。如果你有相应的PDB,你也会有源代码行号。
  • 现在,如果你想添加某些异常的信息,那么当然你可以在精心挑选的地方(也许是实际发生异常的地方,你想要改进你的代码的地方)对于未来的错误),但要确保你真正增加值到原始的(并确保你也将原来的那个作为内部异常,就像你在样本中那样)。

所以,我会说你的示例代码可以没问题。它实际上取决于“自定义错误消息”(可能是异常定制属性 - 确保它们是可序列化的)。它必须增加价值或意义来帮助诊断问题。例如,这对我来说非常好(可以改进):

string filePath = ... ;
try
{
    CreateTheFile(filePath);
    DoThisToTheFile(filePath);
    DoThatToTheFile(filePath);
    ...
}
catch (Exception e)
{
    throw new FileProcessException("I wasn't able to complete operation XYZ with the file at '" + filePath + "'.", e);
}

这不是:

string filePath = ... ;
try
{
    CreateTheFile(filePath);
    DoThisToTheFile(filePath);
    DoThatToTheFile(filePath);
}
catch (Exception e)
{
    throw new Exception("I wasn't able to do what I needed to do.", e);
}

答案 5 :(得分:3)

前言:我认为这种模式只能在某些条件下使用,并且充分了解其优缺点。

<小时/> 声明1:例外情况是成员无法完成应按照名称指示执行的任务。 (Jeffry Richter,CLR来自C#第四版)

声明2:有时我们希望从图书馆成员到a)获得结果或b)告诉我们这是不可能的,我们不关心所有细节,我们只是发送详细信息给图书馆开发者。

来自St.1,St.2的​​结论:当实现库方法时,我们可以包装一般的异常并抛出一个自定义一个,包括源一个作为其InnerException。在这种情况下,使用此成员的开发人员只需捕获一个异常,我们仍将获得调试信息。

<小时/> 案例1:您正在实施一个库方法,并且不希望公开它的内部,或者您打算/假设将来更改它的内部。

public string GetConfig()
{
    try
    {
        var assembly = Assembly.GetExecutingAssembly();
        var resourceName = "MyCompany.MyProduct.MyFile.cfg";

        // ArgumentNullException
        // ArgumentException
        // FileLoadException
        // FileNotFoundException
        // BadImageFormatException
        // NotImplementedException
        using (Stream stream = assembly.GetManifestResourceStream(resourceName))
        // ArgumentException
        // ArgumentNullException
        using (StreamReader reader = new StreamReader(stream))
        {
            // OutOfMemoryException
            // IOException
            string result = reader.ReadToEnd();
        }
        return result;

        // TODO: Read config parameter from DB 'Configuration'
    }
    catch (Exception ex)
    {
        throw new ConfigException("Unable to get configuration", ex);
    }
}

这是一个非常可靠的代码,你确信它不会抛出异常。因为它不应该。你确定吗?或者你将它包装成try-catch,以防万一?或者你让开发人员做这个工作?如果他不关心这种方法是否会成功,他可能有备用计划怎么办?可能是他会调用这个方法来试试try(异常e)而不是你吗?我不这么认为。

优点:

  1. 您隐藏了实施细节,将来可以自由更改;
  2. 来电者不必抓住一大堆不同的例外,如果他们在意,他们可以查看InnerException
  3. 缺点:

    1. 您正在丢失堆栈跟踪(对于第三方库而言可能并不重要);
    2. <小时/> 案例2:您想要向异常添加信息。这不是你在问题中写的代码,但它仍然是一个普通的Exception,我认为这是异常处理工具中被忽视的重要部分。

      catch (Exception ex)
      {
          ex.Data.Add(paramName);
          throw;
      }
      

      <强>增加: 我会用以下方式编辑你的模式:

      try
      {
          // code that can cause various exceptions...
      }
      catch (Exception e)  
      {
          if (e is ThreadAbortException || e is StackOverflowException || e is OutOfMemoryException)
          {                                       
              throw;
          }
      
          throw new MyCustomException("Custom error message", e);
      }
      

      <小时/> 的总结: 在开发库公共方法时,可以使用此模式来隐藏实现细节并简化开发人员的使用。

答案 6 :(得分:1)

Exceptions的一般原则是捕获它们并在本地处理它们(如果适用),否则允许它们冒泡到顶层容器。

这完全取决于运行应用程序的上下文。例如,如果应用程序是ASP.Net Web应用程序,那么IIS中的ASP应用程序服务器可以处理某些异常但是预期的应用程序特定错误(那些是应该捕获并在适当的时候呈现给最终用户。

如果您正在处理I / O,那么有很多因素无法控制(网络可用性,磁盘硬件等),所以如果这里出现故障,那么处理它是个好主意通过捕获异常并向用户显示错误消息直接离开。

最重要的是 不要无声地失败 所以不要将异常包装在你自己的异常中然后忽略它们。例如,最好允许它们冒泡并在Web服务器日志中找到它们。

答案 7 :(得分:1)

在阅读了问题,答案和发表的评论后,我认为Iain Galloway的全面答案可以更广泛地解释其中的大部分内容。

我简短而又甜蜜的解释,

  1. 应该捕获异常,并且只有当您想要隐藏最终用户的技术细节并且只希望用户通过某些正确的消息获知某些内容失败时才会抛出自定义异常但另一方面始终会记录日志文件的相同异常,以便我们可以提供一些技术帮助和帮助用户,如果经常发生相同的情况,并从日志文件中获取我们需要的信息(技术上)。
  2. 你明确地捕获了一些异常类型并且知道确切的场景,在什么情况下该方法抛出异常然后处理它并且有一些预定义的代码可以解释调用该函数的其他方法中的错误并且有一些操作接受错误代码。
    如果你调用很多函数,这可能会有所帮助,并且它们都会抛出一些带有异常代码的预定义异常,这可以帮助对它们进行分类并相应地采取一些行动。
  3. 在您提到的其他一些评论中,如果您明确添加了OutOfMemoryException部分,并且获得详细答案,那么您只能捕获这些异常,那么系统关键异常如[HandleProcessCorruptedStateExceptions]如何?在什么情况下你应该处理它read this SO post

答案 8 :(得分:1)

它主要取决于你捕捉异常的地方。一般来说,库在捕获异常时应该更加保守,而在程序的顶层(例如在主方法中或在控制器中的操作方法的顶部等),你可以更自由地捕获你的东西。

原因是例如您不希望捕获库中的所有异常,因为您可能会掩盖与库无关的问题,例如“OutOfMemoryException”,您真的更喜欢冒泡,以便可以通知用户,等等。如果你正在谈论捕获异常的main()方法中的异常,显示它然后退出......好吧,在这里捕获任何异常可能是安全的。

关于捕获所有异常的最重要规则是,您不应该只是静默地吞下所有异常......例如Java中的类似内容:

try { 
something(); 
} 

catch (Exception ex) {}

或Python中的这个:

try:
something()
except:
pass

因为这些可能是要追查的最难的问题。

一个好的经验法则是,您应该只捕获可以正确处理自己的异常。如果您无法完全处理异常,那么您应该让它冒泡到可以

的人身上

答案 9 :(得分:0)

取决于出现异常时您需要做些什么。如果您希望全局处理所有异常,那么可以将异常重新抛出到调用方法并让它处理异常。但是有很多场景必须在本地处理异常,在这种情况下,应该首选捕获已知类型的异常,并且最后一个catch应该捕获Exception ex(以捕获意外异常)并将异常重新抛出给调用者

try
{
    // code that can cause various exceptions...
}
catch (ArithmeticException e)
{
    //handle arithmetic exception
}
catch (IntegerOverflowException e)
{
    //handle overflow exception
}
catch (CustomException e)
{
    //handle your custom exception if thrown from try{} on meeting certain conditions.
}
catch (Exception e)
{
    throw new Exception(e); //handle this in the caller method
}

答案 10 :(得分:0)

这可能是好习惯,也可能不是取决于场景。虽然我不是这里的专家,据我所知,我认为当你不确定异常时这样做很好。我看到你正在考虑try中可以抛出多个异常的代码。如果您不确定可以抛出哪个异常,那么按照您的方式执行它总是更好。无论如何,执行将不允许您执行多个catch块。每当你遇到第一个阻挡区时,控制将最终到达你想要它去的地方,但绝对不是另一个阻挡区。

答案 11 :(得分:0)

第一个普遍规则 - 永远不要吞下异常!!当您编写一段代码时,您应该能够预测可能存在异常的大多数情况,并且需要通知,这种情况应该在代码中处理。您应该有一个通用日志记录模块,它记录应用程序捕获的任何异常,但实际上可能对用户没有任何用处。

显示与用户相关的错误,例如系统用户可能无法理解IndexOutOfRange异常,但对于我们研究这种情况的原因可能是一个有趣的主题。

我们需要始终知道出了什么问题以及何时我们可以分析根本原因并防止它再次发生,因为如果它可能发生一次。它会再次发生,可能会导致灾难。找到错误的唯一方法是登录应用程序遇到的错误。

答案 12 :(得分:0)

您也可以使用省略号。

   catch (...) 
   {
        // catches all exceptions, not already catches by a catch block before
        // can be used to catch exception of unknown or irrelevant type
   }

除此之外,您可以做的是 嵌套的try-catch

try
{
    //some code which is not for database related 
    try
    {
      //database related code with connection open
    }
    catch(//database related exception)
    {
        //statement to terminate
    }
    **finally()
    {
        //close connection,destroy object
    }**
}
catch(//general exception)
{
    //statement to terminate
}

据我说, 这有助于您更简洁地了解错误类型。

答案 13 :(得分:0)

我认为这个问题非常具有推测性,通常取决于具体情况,但我做了一些研究,我将分享。首先,我想对lain Galloway的代码表达我的看法:

try {
  GetDataFromNetwork("htt[://www.foo.com"); // FormatException?

  GetDataFromNetwork(uriArray[0]); // ArrayIndexOutOfBounds?

  GetDataFromNetwork(null); // ArgumentNull?
}
catch(Exception e)
{
  throw new WeClearlyKnowBetterException(
    "Hey, there's something wrong with your network!", e);
}

如果GetDataFromNetwork可以抛出FormatException,那么这个外部调用应该有自己的方法,在这个方法中将处理该异常,它应该转换为自定义异常,如下所示:

try {
  GetDataFromNetwork();
} catch (FormatException ex) {
  // here you should wrap exception and add custom message, which will specify occuring problem
}

当我为特定应用程序创建自定义异常时,我从Exception扩展MyGeneralException,并且每个更具体的异常都将扩展MyGeneralException。所以,在你进入自定义异常的那一刻,你应该放入方法抛出MyGeneralException。

我正在使用规则,这是我从比我更有经验的开发人员手中接管的,在第一个地方,什么时候可以抛出一些外国异常,那里应该包装成自定义,因为你不想依赖于其他模块的例外。

然后如果您将在任何地方使用方法,您将只将MyGeneralException放入方法签名抛出中,它将在应用程序层中冒泡。它应该在最高级别进行捕获和处理,大多数异常消息用于通过某个处理程序创建响应,或者可以手动处理。

主要在设计异常处理期间,应该考虑,如果您的图书馆将使用第三方开发人员,他们没有兴趣处理许多例外。