C#: 抛出自定义异常的最佳实践

50

我已经阅读了关于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
    }
}

1
只是一个快速提示,FooException 应该作为最佳实践扩展 ApplicationExcetpion。 - David Waters
13
@DavidWaters - 你可能会期望这样,但请查看链接,其中指出:“如果您正在设计一个需要创建自定义异常的应用程序,则建议从Exception类派生自定义异常。最初认为自定义异常应该从ApplicationException类派生; 但实践证明这并没有增加显着的价值。” - Anthony Walsh
4
ApplicationException 被认为是一个错误,根据 http://msdn.microsoft.com/en-us/library/vstudio/ms229064(v=vs.100).aspx 最好扩展 System.Exception。 - krystan honour
8个回答

57

根据我的图书馆经验,你应该将可以预见的所有内容都包裹在一个FooException 中,原因如下:

  1. 人们知道这是来自你的类,或者至少是他们使用它们时的情况。如果他们看到FileNotFoundException,他们可能会四处寻找它。你可以帮助他们缩小范围。(我现在意识到堆栈跟踪服务于此目的,所以这一点可以忽略。)

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

  3. 你的库可以正确地处理清理工作。如果你让异常冒泡,你就强制用户进行清理。如果你已经正确地封装了你正在做的事情,那么他们就不知道如何处理这种情况!

请记住,只包装您可以预测的异常,例如FileNotFound。不要只是包装Exception,指望着最好的结果。


14
我认为这个答案的重要部分是是否提供了更多的上下文信息。如果没有提供此额外的上下文信息,则应该让异常保持不变。 - btlog
1
清理资源指的是处理掉不再需要的资源等。如果我打开一个文件,然后在读取它时出现异常,那么我的代码使用者无法关闭我的文件。我至少需要在其周围加上自己的finally块来完成这个任务。 - Tesserex
1
我不同意第三点。清理工作应该在finally块中处理,而不是在catch块中处理。不过,第一点和第二点是正确的。 - H H
1
@Tess:库应该在管理资源的代码周围放置一个 finally 块。这完全与捕获分离。 - H H
1
@odinserj 这取决于异常是否发生在您的库的公共端或内部。如果用户传递了错误的参数,请给他们 ArgumentException。他们会理解的。如果是您的库在深处做的事情导致了异常,那么您应该将其包装起来。不过,您真正应该做的是修复它,因为这意味着您的库存在漏洞。 - Tesserex
显示剩余7条评论

20

请查看这个MSDN最佳实践指南

如果要重新抛出已捕获的异常,请考虑使用throw而不是throw ex,因为这样可以保留原始的堆栈跟踪(包括行号等信息)。


在这种情况下,我并不试图处理异常,而只是重新包装异常以向下通知堆栈该异常来自于我的类。但这绝对值得注意。 - user295190
1
由于您没有尝试处理我提到的异常,因此您还可以将相同的异常重新抛出到“全局”异常处理类中。然后,您应该知道可以保留原始堆栈跟踪。我的答案更多是一般性的建议/提示。 - Tim Schmelter
3
@TimSchmelter:信息不错,但这并不是这个问题的相关答案。看起来您在标题后停止阅读了。在这种情况下,您的建议没有任何收获;您没有理由捕获异常(您的throw;与OP的第二种选项相当于直接跳过,因为他没有进行任何其他处理)。 - Travis Watson
问题在于,这里,OP 对于异常处理的整个思考方式是有缺陷的。他实际上没有处理异常。通过查看堆栈跟踪来“知道它来自我的库”,而不是通过使用包装器混淆实际异常类型。应该在“Finally”块中执行“清理”代码,这不需要存在“Catch”块。 - Mike U

6

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

在数据访问层中,我捕获每个错误的级别仍然可以捕获存储过程的名称和传递的参数的值。或内联SQL。也许是数据库名称或部分连接字符串(请勿包含凭据)。它们可能会放在Message中或者它们自己的新的自定义DatabaseInfo属性中。

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

因此,根据您的问题,我的策略是:只有在我能够处理异常时才捕获。而很多时候,我所能做的只是记录详细信息。因此,我仅在这些详细信息可用的点处捕获,然后重新抛出以使异常上升到UI。并且我在自定义异常中保留原始异常。


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

1
为什么要捕获异常,只是为了将其重新封装在另一个异常中? - btlog
6
@btlog:提供更好的信息(不丢失旧信息)。 - H H

1
代码在捕获异常时最重要的一点是要知道系统状态相对于它“应该”是什么(假设异常被抛出是因为有些东西出了问题),但不幸的是,这完全没有体现在异常对象中。如果在LoadDocument方法中发生错误,假设文档没有成功加载,但至少有两种可能的系统状态:
  1. 系统状态可能就像从未尝试过一样。在这种情况下,如果应用程序可以在没有加载文档的情况下继续运行,那么这将是完全正确的。
  2. 系统状态可能已经受到足够的破坏,最好的做法是保存可以保存的内容到“恢复”文件中(避免用可能损坏的数据替换用户的好文件)并关闭。
显然,在这两个极端之间通常会有其他可能的状态。我建议应该努力创建一个自定义异常,明确指出状态#1的存在,并且如果可以预见但无法避免的情况可能导致状态#2,则可能还需要一个异常。任何导致状态#1的异常都应该包装在指示状态#1的异常对象中。如果异常可能以危及系统状态的方式发生,它们应该被包装为#2或允许其上浮。

1

如果您在Visual Studio中运行“异常”代码片段,则可以获得一个良好实践异常编写的模板。


1

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

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

2
我认为他在选项1中的第一次抛出并不是要被捕获的。这只是另一种情况的示例,他可以抛出自己的异常。例如,“Something bad”可能只是参数为负数。 - Tesserex

0

选项2是最好的。我认为最佳实践是只在计划对异常进行处理时才捕获异常。

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


但是很少有调用者想要拦截ArgumentException。捕获FooException可能更有意义。 - H H

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接