设计异常解决方案的几点注意事项

……”描述的是总要遵循的规范(但特殊情况下,可能需要违反)。

考虑……”描述的是一般情况下应该遵循的规范,但如果完全理解规范背后的道理,并有很好的理由不遵循它时,也不要畏惧打破常规。

不要……”描述的是一些几乎绝对不该违反的规范。

避免……”则没有那么绝对,它描述的是那些通常并不好,但却存在一些已知的可以违反的情况。

× 不要返回错误码。

前面第1节已经讨论了异常的种种好处,所以还是把异常作为报告错误的主要方法。记住每个异常都有两种信息:其一是异常信息(Message属性),其二是异常的类型,异常处理程序根据它来决定应该执行什么操作。

通过抛出异常的方式来报告操作失败。

如果一个方法未能完成它应该完成的任务,那么应该认为这是方法层面的操作失败,并抛出异常。

考虑通过调用System.Environment.FailFast(New in .NET 2.0)来终止进程,而不要抛出异常,如果代码遇到了严重问题,已经无法继续安全地执行

× 不要在正常的控制流中使用异常,如果能够避免的话。

考虑抛出异常可能会对性能造成的影响,详见第7节。

为所有的异常撰写文档,异常本质上是对程序接口隐含假设的一种违反。我们显然需要对这些假设作详细的文档,以减少用户代码引发异常的机会。

× 不要让公有成员根据某个选项来决定是否抛出异常。

例如:

// 不好的设计
public Type GetType(string path, bool throwOnError)

调用者要比方法设计者更难以决定是否抛出异常。

× 不要把异常用作公有成员的返回值或输出参数。

这样会丧失用异常来报告操作失败的诸多好处。

× 避免显式地从finally代码块中抛出异常。

考虑优先使用System命名空间中已有的异常,而不是自己创建新的异常。

使用自定义的异常类型,如果对错误的处理方式与其它已有异常类型有所不同。

关于创建自定义异常类的的细节见第5节。

× 不要仅仅为了拥有自己的异常而创建并使用新的异常。

使用最合理、最具针对性的异常。

抛出System.Exception总是错的,如果这么做了,那么就想一想自己是否真地了解抛出异常的原因。

在抛出异常时提供丰富而有意义的错误消息。

要注意的是这些信息是提供给谁的,可能是其它开发人员,也可能是最终用户,所以这些信息应根据面向的对象设计

确保异常消息的语法正确无误(指自然语言,如汉语、英语等)。

确保异常消息中的每个句子都有句号。

这个看起来似乎过于追究细节了,那么想想这种情况:使用FCL预定义异常的Message信息时,我们有没有加过句号。如果我们提供的信息没有句号,其它开发人员使用时到底加不加句号呢?

× 避免在异常消息中使用问号和感叹号。

或许我们习惯于使用感叹号来”警示”某些操作有问题,扪心自问,我们使用的代码返回一个感叹号,自己会有什么感觉。

× 不要在没有得到许可的情况下在异常消息中泄漏安全信息。

考虑把组件抛出的异常信息本地化,如果希望组件为使用不用(自然)语言的开发人员使用。

6.2 处理异常

根据6.1节的讨论,我们可以决定何时抛出异常,然后为之选择合适的类型,设计合理的信息,下一步就是如何处理异常了。

如果用catch语句块捕获了某个特定类型的异常,并完全理解在catch块之后继续执行对应用程序意味着什么,那么我们说这种情况是对异常进行了处理。

如果捕获的异常具体类型不确定(通常都是如此),并在不完全理解操作失败的原因或没有对操作失败作出反应的情况下让应用程序继续执行,那么我们说这种情况是把异常吞了

× 不要在框架(是指供开发人员使用的程序)的代码中,在捕获具体类型不确定的异常(如System.Exception、System.SystemException)时,把异常吞了。

× 避免在应用程序的代码中,在捕获具体类型不确定的异常(如System.Exception、System.SystemException)时,把错误吞了。

有时在应用程序中把异常吞了是可以接受的,但必须意识到其风险。发生异常通常会导致状态的不一致,如果贸然将异常吞掉,让程序继续执行下去,后果不堪设想。

× 不要在为了转移异常而编写的catch代码块中把任何特殊的异常排除在外。

考虑捕获特定类型的异常,如果理解异常产生的原因并能对错误做适当的反应

此时一定要能够确信,程序能够从异常中完全恢复

× 不要捕获不应该捕获的异常。通常应允许异常沿调用栈向上传递。

这一点极为重要。如果捕获了不该捕获的异常,会让bug更难以发现。在开发、测试阶段应当把所有bug暴露出来。

在进行清理工作时使用try-finally,避免使用try-catch。

对于精心编写的代码来说,try-finally的使用频率要比try-catch要高的多。这一点可能有违于直觉,因为有时可能会觉得:try不就是为了catch吗?要知道一方面我们要考虑程序状态的一致,另一方面我们还需要考虑资源的清理工作。

在捕获并重新抛出异常时使用空的throw语句。这是保持调用栈的最好方法。

如果捕获异常后抛出新的异常,那么所报告的异常已不再是实际引发的异常,显然这会不利于程序的调试,因此应重新抛出原来的异常。

× 不要用无参数的catch块来处理不与CLS兼容的异常(不是继承自System.Exception的异常)。

有时候让底层代码抛出的异常传递到高层并没有什么意义,此时,可以考虑对底层的异常进行封装使之对高层的用户也有意义。还有一种情况,更重要的是要知道代码抛出了异常,而异常的类型则显得无关紧要,此时可以封装异常。

考虑对较低层次抛出的异常进行适当的封装,如果较低层次的异常在较高层次的运行环境中没有什么意义。

× 避免捕获并封装具体类型不确定的异常。

在对异常进行封装时为其指定内部异常(inner exception)。

这一点极为重要,对于代码的调试会很有帮助。

6.3 标准异常类型的使用

× 不要抛出Exception或SystemException类型的异常。

× 不要在框架(供其它开发人员使用)代码中捕获Exception或SystemException类型的异常,除非打算重新抛出。

× 避免捕获Exception或SystemException类型的异常,除非是在顶层的异常处理器程序中。

× 不要抛出ApplicationException类型的异常或者从它派生新类(参看4.2描述)。

抛出InvalidOperationException类型的异常,如果对象处于不正确的状态。

一个例子是向只读的FileStream写入数据。

抛出ArgumentException或其子类,如果传入的是无效参数。要注意尽量使用位于继承层次末尾的类型。

在抛出ArgumentException或其子类时设置ParamName属性。

该属性表明了哪个参数引发了异常。

public static FileAttributes GetAttributes(string path)
    {
if (path == null)
        {
throw new ArgumentNullException("path", clip_image001);
        }
    }

在属性的设置方法中,以value作为隐式值参数的名字。

public FileAttributes Attributes
    {
set
        {
if (value == null)
            {
throw new ArgumentNullException("value", clip_image001[1]);
            }
        }
    }

× 不要让公用的API抛出这些异常。

抛出这些异常会暴露实现细节,而细节可能会随时间变化。

另外,不要显式地抛出StackOverflowException、OutOfMemeryException、ComException、SEHException异常,应该只有CLR才能抛出这些异常。

7、性能方面的考虑

我们在使用异常时常常会产生性能方面的顾虑,在调试的时候感觉尤其明显。这样的顾虑合情合理。当成员抛出异常时,对性能的影响将是指数级的。当遵循前面的规范,我们仍有可能获得良好的性能。本节推荐两种模式。

7.1 Tester-Doer 模式

有时候,我们可以把抛出异常的成员分解为两个成员,这样就能提高该成员的性能。下面看看ICollection<T>接口的Add方法。

ICollection<int> numbers = …
numbers.Add(1);

如果集合是只读的,那么Add方法会抛出异常。在Add方法经常会失败的场景中,这可能会引起性能问题。缓解问题的方法之一是在调用Add方法前,检查集合是否可写。

ICollection<int> numbers = …

if(!numbers.IsReadOnly)
{
numbers.Add(1);
}

用来对条件进行测试的成员成为tester,这里就是IsReadOnly属性;用来执行实际操作并可能抛出异常的成员成为doer,这里就是Add方法。

考虑在方法中使用Test-Doer模式来避免因异常而引发的性能问题,如果该方法在普通的场景中都可能会抛出异常(引发异常的频率较高)。

前提是”test”操作要远比”do”操作快。另外要注意,在多线程访问一个对象时会有危险性。

7.2 Try-Parse 模式

与Tester-Doer 模式相比,Try-Parse 模式甚至更快,应在那些对性能要求极高的API中使用。该模式对成员的名字进行调整,使成员的语义包含一个预先定义号的测试。例如,DateTime定义了一个Parse方法,如果解析字符串失败,那么它会抛出异常,同时还提供了一个与之对应的TryParse方法,在解析失败时会返回false,成功时则通过一个输出参数来返回结果。

使用这个模式时注意,如果因为try操作之外的原因导致(方法)操作失败,仍应抛出异常。

考虑在方法中使用Try-Parse模式来避免因异常而引发的性能问题,如果该方法在普通的场景中都可能会抛出异常。

posted @ 2009-04-17 00:02  James.Ying  阅读(2492)  评论(2编辑  收藏  举报