【问题标题】:C#: Throwing Custom Exception Best PracticesC#:抛出自定义异常最佳实践
【发布时间】:2011-06-13 06:30:09
【问题描述】:

我已经阅读了一些关于 C# 异常处理实践的其他问题,但似乎没有人问我在寻找什么。

如果我为特定类或一组类实现我自己的自定义异常。与这些类相关的所有错误都应该使用内部异常封装到我的异常中,还是应该让它们通过?

我认为最好捕获所有异常,以便可以立即从我的源中识别出异常。我仍然将原始异常作为内部异常传递。另一方面,我认为重新抛出异常是多余的。

例外:

class FooException : Exception
{
    //...
}

选项 1:Foo 封装所有异常:

class Foo
{
    DoSomething(int param)
    {
        try 
        {
             if (/*Something Bad*/)
             {  
                 //violates business logic etc... 
                 throw new FooException("Reason...");
             }
             //... 
             //something that might throw an exception
        }
        catch (FooException ex)
        {
             throw;
        }
        catch (Exception ex)
        {
             throw new FooException("Inner Exception", ex);
        }
    }
}

选项 2:Foo 抛出特定的 FooExceptions 但允许其他异常通过:

class Foo
{
    DoSomething(int param)
    {
        if  (/*Something Bad*/)
        {
             //violates business logic etc... 
             throw new FooException("Reason...");
        }
        //... 
        //something that might throw an exception and not caught
    }
}

【问题讨论】:

  • 只是一个简短的说明 FooException 应该扩展 ApplicationExcetpion 作为最佳实践。
  • @DavidWaters - 您可能希望是这种情况,但请参阅:link,其中声明“如果您正在设计一个需要创建自己的异常的应用程序,您建议从 Exception 类派生自定义异常。最初认为自定义异常应该从 ApplicationException 类派生;但在实践中并未发现这会增加显着价值。”
  • ApplicationException 已被确认为有点错误,最好按照msdn.microsoft.com/en-us/library/vstudio/…扩展 System.Exception

标签: c# exception custom-exceptions


【解决方案1】:

根据我在库方面的经验,出于以下几个原因,您应该将所有内容(您可以预期的)包装在 FooException 中:

  1. 人们知道它来自您的课程,或者至少是他们对它们的使用。如果他们看到FileNotFoundException,他们可能正在到处寻找它。你正在帮助他们缩小范围。 (我现在意识到堆栈跟踪服务于这个目的,所以也许你可以忽略这一点。)

  2. 您可以提供更多上下文。用您自己的异常包装 FNF,您可以说“我试图加载此文件为此目的,但找不到它。这暗示了可能的正确解决方案。

    李>
  3. 您的库可以正确处理清理。如果你让异常冒泡,你就是在强迫用户清理。如果你正确地封装了你在做什么,那么他们不知道如何处理这种情况!

请记住只包装您可以预期的异常,例如FileNotFound。不要只是包装Exception 并希望最好。

【讨论】:

  • 我认为这个答案的重要部分是您是否提供了更多上下文。如果你没有提供这个额外的上下文,那么让异常保持不变。
  • 清理,我的意思是处理资源等。如果我打开一个文件,然后在读取它时出现异常,我的代码用户可能无法关闭我的文件。我至少需要有自己的 finally 块才能做到这一点。
  • 我不同意第 3 点。清理是用 finally 块来解决的,而不是用 catch 来解决的。不过第 1) 点和第 2) 点是正确的。
  • @Tess:库应该在管理资源的代码周围放置一个 finally 块。这与捕捉完全分开。
  • @odinserj 这取决于异常发生在库的公共端还是内部。如果用户传入了错误的参数,请给他们ArgumentException。他们会明白的。如果你的图书馆在深处做了什么导致它,你会把它包装起来。再说一次,你真正应该做的是修复它,因为这意味着你的库有一个错误。
【解决方案2】:

看看这个MSDN-best-practises。

如果您想重新抛出捕获的异常,请考虑使用throw 而不是throw ex,因为这样会保留原始堆栈跟踪(行号等)。

【讨论】:

  • +1 在这种特殊情况下,我不尝试处理异常,只是重新包装异常以通知堆栈它来自我的班级。但绝对值得注意。
  • @snmcdonald:因为您没有尝试处理我提到的异常,所以您也可以将相同的异常重新抛出到“全局”异常处理类。然后您应该知道可以保留原始堆栈跟踪。我的回答更多的是一般性建议/提示。
  • @TimSchmelter:很好的信息,但这不是这个问题的相关答案。您似乎在标题后停止阅读。在这种情况下,您的建议一无所获;您无缘无故地发现了异常(您的 throw; 相当于 OP 的选项 2 失败,因为他没有进行任何额外的处理)。
  • 这里的问题是,操作员对异常处理的整个思考是有缺陷的。他实际上并没有处理异常。 “知道它来自我的库”是通过查看堆栈跟踪来完成的,而不是通过使用包装器混淆实际的异常类型。 “清理”代码应在“Finally”块中完成,不需要存在“Catch”块。
【解决方案3】:

在创建自定义异常时,我总是添加几个属性。一种是用户名或 ID。我添加了一个 DisplayMessage 属性来携带要显示给用户的文本。然后,我使用 Message 属性来传达要记录在日志中的技术细节。

我捕获数据访问层中的每个错误,我仍然可以捕获存储过程的名称和传递的参数的值。或内联 SQL。也许是数据库名称或部分连接字符串(请不要提供凭据)。这些可能会出现在 Message 或他们自己的新自定义 DatabaseInfo 属性中。

对于网页,我使用相同的自定义异常。我将在 Message 属性中放入表单信息——用户在网页上的每个数据输入控件中输入的内容、正在编辑的项目的 ID(客户、产品、员工等)以及用户的操作发生异常时正在服用。

因此,根据您的问题,我的策略是:仅在我可以对异常采取措施时才捕获。很多时候,我所能做的就是记录细节。所以,我只在这些细节可用的地方捕获,然后重新抛出以让异常冒泡到 UI。我在我的自定义异常中保留了原始异常。

【讨论】:

    【解决方案4】:

    自定义异常的目的是为堆栈跟踪提供详细的上下文信息以帮助调试。选项 1 更好,因为没有它,如果异常发生在堆栈中的“较低”位置,您将无法获得异常的“起源”。

    【讨论】:

    • 为什么要捕获一个异常,只是将它重新包装在另一个异常中?
    • @btlog:提供更好的信息(不丢失旧信息)。
    【解决方案5】:

    如果您在 Visual Studio 中为“异常”运行代码 sn-p,您将拥有一个编写异常的良好实践模板。

    【讨论】:

      【解决方案6】:

      注意 选项 1:您的 throw new FooException("Reason..."); 不会被捕获,因为它位于 try / catch 块之外

      1. 您应该只捕获要处理的异常。
      2. 如果您没有向异常添加任何其他数据,请使用throw;,因为它不会杀死您的堆栈。在选项 2 中,您仍然可以在 catch 中进行一些处理,然后调用 throw; 以使用原始堆栈重新抛出原始异常。

      【讨论】:

      • 我认为他在选项 1 中的第一次投掷并不意味着被抓住。这只是他可以抛出自己的异常的另一种情况的示例。例如,“不好的事情”可能只是参数为负数。
      【解决方案7】:

      当捕获异常时,代码要知道的最重要的事情(不幸的是,Exception 对象完全丢失了该异常)是系统相对于它“应该”的状态(推测异常是因为发生了某些事情而引发的)错误的)。如果 LoadDocument 方法发生错误,推测文档没有加载成功,但至少有两种可能的系统状态:

      1. 系统状态可能就像从未尝试过加载一样。在这种情况下,如果应用程序可以在没有加载的文档的情况下继续运行,那么它是完全正确的。
      2. 系统状态可能已严重损坏,最好的做法是将可以保存的内容保存到“恢复”文件(避免用可能损坏的数据替换用户的好文件)并关闭。

      显然,在这些极端之间通常还会有其他可能的状态。我建议人们应该努力有一个自定义异常,明确表明状态 #1 存在,如果可预见但不可避免的情况可能导致状态 #2,则可能有一个。任何发生并将导致状态 #1 的异常都应包装在指示状态 #1 的异常对象中。如果异常可能以可能危及系统状态的方式发生,则应将它们包装为 #2 或允许其渗透。

      【讨论】:

        【解决方案8】:

        选项 2 是最好的。我相信最佳实践是仅在您计划对异常执行某些操作时才捕获异常。

        在这种情况下,选项 1 只是用您自己的异常包装一个异常。它没有增加任何价值,你的类的用户不能再仅仅捕获 ArgumentException,例如,他们还需要捕获你的 FooException 然后对内部异常进行解析。如果内部异常不是异常,他们能够做一些有用的事情,他们需要重新抛出。

        【讨论】:

        • 但是调用者不太可能想要拦截 ArgumentException。捕获 FooException(s) 可能更有意义。
        猜你喜欢
        • 1970-01-01
        • 2010-10-12
        • 1970-01-01
        • 2010-12-02
        • 1970-01-01
        • 2010-10-15
        • 2011-12-24
        • 2018-08-08
        • 1970-01-01
        相关资源
        最近更新 更多