理解.NET中的异常

178 篇文章 0 订阅
11 篇文章 0 订阅
或许从第一次使用异常开始,我们就要经常考虑诸如何时捕获异常,何时抛出异常,异常的性能如何之类的问题,有时还想了解究竟什么是异常,它的机制又是什么。本文试着对这些问题进行讨论。


主要内容包括:

为什么使用异常    主要讨论异常与错误码之间的选择

异常的本质        异常的概念的理解   

异常的机制        try,catch,finally三种语句块的讨论

System.Exception及其它FCL中的异常类   讨论.NET Framework中预定义的异常类型

自定义异常类      如何建立自定义的异常类型

正确地使用异常    关于异常使用的一些规范和约定

性能问题的考虑    了解异常对性能的影响,并给出一些建议

应用程序中的未处理异常; 如何处理程序中的那些未处理的异常

1、为什么要使用异常

通常在处理程序中的错误时,可以使用异常,也可以使用返回值的方式。与用返回值来报告错误相比,异常处理有诸多优势:

异常已经很好地与面向对象语言集成在一起

在某些情况下,如构造函数、操作符重载及属性,开发人员对返回值没有什么选择的余地。想在面向对象框架中统一使用返回值来报告错误是不可能的。在上述情况下,唯一的选择是使用返回值之外的方法,如异常。既然如此,最好的办法就是在所有地方都使用异常来报告错误。这是必须用异常来报告错误的最重要原因(Jeffery Richter语)。

异常增强了API的一致性

异常的唯一目的就是为了报告错误,而返回值则有多种用途,报告错误只是其中之一。因此如果使用异常,那么报告错误的方式是固定的,这保证了API的一致性。

更容易使错误处理的代码局部化,错误处理的代码可以放在一个更集中的位置。

有很多原因可以导致代码失败,如引用null对象、索引越界、访问已关闭的文件、内存耗尽等等。不使用异常处理,要使我们的代码容易地检测这些原因、并从中恢复过来,即使有可能,也是很困难的。同时要进行这种检测的代码需要散布在程序主逻辑中,使得编码过程变得非常困难,代码也难以理解和维护。

如果使用异常处理,我们就无需再自己编写代码来检测这些潜在的故障。我们只管放心地编码,并“自信地”认为代码没有问题,这个过程自然会变得简单,代码也容易理解和维护。最后将异常恢复代码放在一个集中的位置(try代码块的后面,或者调用栈的更高层),当程序出现故障时,才会执行这些恢复代码。

使用异常,我们可以将资源清理代码放在一个固定的位置,并确保得到执行。

将资源清理代码从应用程序的主逻辑移到一个固定的位置后,应用程序将会变得更容易编写、理解和维护。

容易定位和修复代码中的Bug

一旦程序出现了问题,CLR会遍历线程的调用堆栈,以查找能够处理该异常的代码。如果找不到这样的代码,我们将收到一个开发人员很不愿看到的“未处理异常”通知。根据这个通知,可以很容易地定位问题发生地点,判定原因,并修复Bug。

错误码容易被忽略,而且通常会被忽略。而异常则不然

异常允许用户定义未处理异常的处理程序,那错误码呢?

另外,异常有助于与一些工具的交互

各种工具(如调试器、性能分析器、性能计数器等)会时刻注意异常的发生。返回错误码的方法就没有这些好处。

看了这些比较之后,你 还会选择错误码吗?

2、异常的本质

异常本质上是对程序接口隐含假设的一种违反

在设计一个类型时,我们首先应设想该类型被应用的各种场景。类型的名称通常是一个名词,如FileStream,StringBuilder等。然后再为类型定义属性、方法及事件等成员。这些成员的定义方式(属性的数据类型,方法的参数、返回值等)就是类型的程序接口。同时,这些成员描述了类型(或其实例)所能执行的操作,它们的名称通常为动词,如Read,Write,Flush,Append,Insert,Remove等。

对于类型的一个成员来说,如果不能完成它的任务,就应当抛出异常。异常意味着类型的成员未能完成其名称所描述的功能

我们定义的程序接口通常会有一些隐含假设,如System.IO.File.OpenText(string path)方法,它的功能是打开现有的UTF-8编码文本文件以进行读取,要完成该功能,就要求path参数是一个合法的路径、指定的文件存在、有足够的权限等。所有这些要求在MSDN文档中都有所记录,我们在调用该方法时可以详细地查看文档,以最高效的方式来调用它。但我们都了解,这不太现实,我们不太可能去了解所有的这些隐含的假设,从而也就不可避免地违反它们了。

那么,对于OpenText方法的开发人员来说,如何通知调用它的程序,隐含假设被违反了呢?答案是抛出一个异常(你也许会想到返回值,关于返回值和异常的取舍请参看第1节)。

所以在设计一个类型时,我们应该首先假设类型最常见的使用方式,然后设计其接口使之能够很好地处理这种情况。最后再考虑接口带来的隐含假设,并且当任何假设被违反时便抛出异常

这样理解下来,有些观点正确与否就很清楚了。

我们把注意力放在前面提及的OpenText方法,来看看下面两种关于异常的误解。

异常与事情发生的频率有关

OpenText方法的开发人员决定何时抛出异常,但真正引发异常的却是调用代码,那OpenText方法的开发人员又怎么知道调用代码引发异常的频率?

异常就是错误

错误意味着程序员犯错了,当调用代码在应用程序中错误地调用了OpenText方法时,设计该方法的开发人员何从知道呢? 只有调用程序才能判断调用的结果是否出现了错误。换言之,异常是OpenText方法对调用程序的一种反馈,而调用的结果是否为错误则是由调用程序判断的,是与调用程序的上下文环境相关的。

3、异常的机制

下面的C#代码展示了异常处理的标准用法,从总体上描述了异常处理的样子和用途。在代码之后详细讨论了try、catch、finally三种语句块。

 

try
{
    
//  在此处编写那些需要恢复或清理操作的代码
}
catch  (NullReferenceException)
{
    
//  在此处编写能够从NullReferenceException(或其派生类型异常)中恢复的代码
}
catch  (Exception)
{
    
//  我们在这个块中编写能够从任何与CLS兼容的异常中恢复的代码

    
//  另外,此时通常应将其重新抛出
     throw ;
}
catch
{
    
//  我们在这个块中编写能够从任何与CLS兼容或者不兼容的异常中恢复的代码

    
//  此时通常应将其重新抛出
     throw ;
}
finally
{
    
//  在finally块中我们放入那些对try块中启动的操作进行清理的代码。
    
//  不管是否有异常抛出,此处代码总是执行。
}

 

3.1 try

try块中包含的通常是需要进行清理或/和异常恢复的操作。所有的资源清理代码都应该放在一个finally块中(以确保总是得到执行)。try块还可以包含可能抛出异常的代码。进行异常恢复操作的代码则应该放在一个或多个catch块中。一个try块必须有至少一个与之相关联的catch块或finally块,单独一个try块是没有意义的(C#编译器也不允许你这么做)。

3.2 catch

catch块中包含的是出现异常时需要执行的相应代码。一个try块可以有0个或多个catch块与之相关联。

如果try块中的代码没有抛出异常,CLR就不会执行与该try块关联的catch块的代码。而是会跳过所有的catch块,直接执行finally块(如果存在的话)中的代码,然后再执行finally块之后的语句。

catch关键字后面的表达式称作异常筛选器(exception filter)。它表示开发人员预料到的、并可以从中恢复的一种异常情况。代码执行时是自上而下搜索catch块的,因此要将更具体的(是指该类型在继承体系中的层次)异常放在上面。事实上,C#编译器不允许更具体的catch块出现在离代码底部更近的位置。

如果try块(或者被try块调用的方法)中的代码抛出了一个异常,CLR将搜索那些筛选器中能够识别该异常的catch块。如果如该try块相关联的筛选器中没有一个能够接受该异常,CLR将沿着调用堆栈向更高层搜索能够接受该异常的筛选器,如果直到堆栈顶部依然没有找到能够处理该异常的catch块,就会出现所谓的未处理异常

一旦CLR找到了一个能够处理所抛出异常的筛选器,它将执行从抛出异常的try块开始,到匹配异常的catch块为止的范围内所有的finally块(注意要在调用堆栈中理解,此处执行的代码不包括匹配异常的catch块相关联的finally块),然后调用匹配catch块中的代码,最后才是匹配catch块相关联的finally块的代码。

在C#中,异常筛选器可以指定一个异常变量。当捕获到一个异常时,该变量指向那个被抛出的、类型继承自System.Exception的对象,此时可以通过该变量获取异常的相关信息(如其堆栈踪迹)。尽管可以改变该对象,但不应这么做,应把它当作只读变量。

catch块中的代码一般执行一些从异常中恢复的操作。在catch块的末尾(比如进行恢复操作后),我们有三种选择:

  • <!--[if !supportLists]--><!--[endif]-->重新抛出所捕获的异常,向更高一层的调用堆栈中的代码通知该异常的发生。

  • <!--[if !supportLists]--><!--[endif]-->抛出一个不同的异常,向更高一层的调用堆栈中的代码提供更多信息。

  • <!--[if !supportLists]-->让线程从catch块的底部退出。

如果我们选择前两种方法,将抛出一个异常,CLR的行为将和处理前面的异常时一样,遍历调用堆栈搜索合适的筛选器。如果选用第三种方法,会将异常吞掉,更高层次的调用堆栈将不会知晓异常的发生,在执行完catch块中的代码后,立即执行与之关联的finally块(如果存在的话),然后执行当前try/catch/finally语句块之后的代码。

关于这三种方法的选择,将在后面讨论。

3.3 finally

finally块中包含的是确保要执行的代码。一般地,finally块中的代码执行的是一些资源清理操作,这些清理操作通常是try块中的行为所需要的。例如,我们在try块中打开一个文件,那么我们就应该将关闭文件的代码放在与其对应的finally块中。

FileStream fs  =   null ;
try
{
    fs 
=   new  FileStream(fileName, FileMode.Open);
}
catch  (OverflowException)
{
    
//  恢复操作的代码
}
finally
{
    
if  (fs  !=   null )
    {
        fs.Close();
    }
}


清理资源的代码不能放在finally块之后,如果出现了异常,但catch块未能捕获,那么这些代码就不会执行了。

try块也并非总需要finally块,有时候try块的操作并不需要任何清理工作。

这里将讨论FCL中预定义的异常类,自定义异常类,正确的使用异常(抛出、捕获、封装),最后给出性能方面的建议。

4、System. Exception及其它FCL中的异常类

4.1 System.Exception 类型

CLR允许我们将任何类型——Int32、String等——的一个实例作为异常抛出。但实际上,微软定义了System.Exception类型,并规定所有和CLS兼容的编程语言都必须能够抛出并捕获那些继承自System.Exception的异常类型。继承自System.Exception的异常类型被认为是与CLS兼容的。C#和其它许多语言都只允许代码抛出与CLR兼容的异常。

System.Exception类型是一个很简单的类型,下表列出了它所包含的一些属性。

属性

访问权限

类型

描述

Message

只读

String

包含一段辅助性的文本,描述异常发生的原因。在出现未处理异常时,这些信息通常会写入log。这些信息用户通常是看不见的,所以应尽量使用技术性的词汇以帮助其它开发人员修正代码。

Data

只读

IDictionary

一个指向key-value对集合的引用。通常应在抛出异常前,向该集合添加信息,而捕获异常的代码则使用这些信息进行异常恢复操作。

Source

读写

String

产生异常的程序集的名称

StackTrace

只读

String

包含了调用堆栈中抛出异常的方法的名称和签名。该属性对于调试极具价值。

TargetSite

只读

MethodBase

抛出异常的方法。

HelpLink

读写

String

获取或设置异常的关联帮助文件的链接。

InnerExceptoin

只读

Exception

如果当前异常是在处理另一个异常时产生的,那么该属性表示前一个属性。该属性通常为null。Exception类型还提供了一个公有方法GetBaseException,用以遍历所以内部异常组成的链表,返回最开始那个异常。

4.2 FCL中的异常类结构

.NET框架类库(FCL)中定义了很多异常类型(而它们最终又都继承自Exception类)。

微软的最初想法是这样的:System.Exception应是所有异常的基类型,另两个类型System.SystemException和System.ApplicationException则是仅有的直接继承自System.Exception的异常类型。此外,CLR抛出的异常都继承自SystemException,应用程序抛出的异常应当继承自ApplicationException。这样一来,开发人员就能够编写catch块来捕获所有CLR抛出的异常或所有应用程序抛出的异常。

但遗憾的是,FCL也没能很好地遵循这个原则。有些异常类直接继承自Exception(IsolatedStorageException),有些CLR抛出的异常却继承自ApplicationException(如TargetInvocationException),还有些应用程序抛出的异常则继承自SystemException(如FormatException)。这显得有些混乱,而其结果便是SystemException和ApplicationException类型的存在没有多大价值了。

5、自定义异常类

如果FCL没有为我们定义合适的异常类型,我们就要考虑定义创建自己的异常类了。

一般情况下,自定义异常类应继承自Exception类型或其它与Exception相近的基类型,如果我们定义的类型不打算作为其它类型的基类型,应该将其标识为sealed。

Exception基类型定义了4个构造函数:

  • <!--[if !supportLists]--><!--[endif]-->公有的无参(默认)构造函数,创建一个异常类型的实例,并将所有字段和属性的值设置为默认值。
  • <!--[if !supportLists]-->公有的带一个String参数的构造函数,创建一个异常类型的实例,异常的消息将设置为参数指定的文本。
  • <!--[if !supportLists]-->公有的带String和Exception参数的构造函数,创建一个异常类型的实例,并设置其消息文本和内部异常。在对异常进行封装时,该构造函数会显示出其重要性
  • <!--[if !supportLists]--><!--[endif]-->受保护的带SerializationInfo和StreamingContext参数的构造函数,反序列化Exception对象的实例。记住,如果异常类型是密封的(sealed),该方法应声明为private的,确保该构造函数能够调用基类型的相同的构造函数,这样基类的字段能够得到正确的序列化。

在定义自己的异常类型时,我们应当实现这四个构造函数,而且它们都要调用基类型中相应的构造函数。

当然,我们定义的异常类型会继承Exception类型的所有字段和属性。此外,我们还可以为其添加自己的字段和属性。例如,System.ArgumentException中添加了一个虚拟的(virtual)String属性ParamName(其它的一切则继承自Exception类型)。ArgumentException也定义了两个新的构造函数(除了上述四个),来初始化ParamName属性。这里我们也应了解,如果为异常类型添加字段,要确保添加必要的构造函数来初始化这些字段,同时也要定义相应的属性或其它成员(如方法)返回这些字段的值

所有的异常类型(继承自Exception类型)都应是可序列化的,只有这样异常对象才能在跨越应用程序域时得到封送处理(marshaled),同时还能持久化至日志或数据库。要使自定义的异常类型可序列化,首先为该类型应用Serializable特性(attribute);如果类型定义了新的字段,我们还必须让其实现ISerilizable接口的GetObjectData方法和上面提及的受保护的构造函数。

下面是的示例演示了如何正确地创建自定义异常类型:

要实现上面四个构造函数,建议使用VS 2005中的Code Snippet:



按两下Tab后,VS就会为你生成类的基本结构:),我们在这个基础上编写代码。

下面是类的完整代码:

     //  为类应用Serializable特性
    [Serializable]
    
public   sealed   class  CustomException : Exception
    {
        
//  添加的自定义字段
         private   string  stringInfo;
        
private   bool  booleanInfo;

        
//  实现三个公有的构造函数,这里只是简单地调用基类的构造函数
         public  CustomException() { }

        
public  CustomException( string  message) :  base (message) { }

        
public  CustomException( string  message, Exception inner) 
            : 
base (message, inner) { }

        
//  实现ISerialization接口所需要的反序列化构造函数。
        
//  因为本类为sealed,该构造函数为private。
         private  CustomException(SerializationInfo info, StreamingContext context)
            : 
base (info, context) 
        {
            stringInfo 
=  info.GetString( " StringInfo " );
            booleanInfo 
=  info.GetBoolean( " BooleanInfo " );
        }

        
//  添加构造函数以确保自定义字段能得到正确的初始化
         public  CustomException( string  message,  string  stringInfo,  bool  booleanInfo)
            : 
base (message)
        {
            
this .stringInfo  =  stringInfo;
            
this .booleanInfo  =  booleanInfo;
        }

        
public  CustomException( string  message, Exception inner,  string  stringInfo,  bool  booleanInfo)
            : 
base (message, inner)
        {
            
this .stringInfo  =  stringInfo;
            
this .booleanInfo  =  booleanInfo;
        }

        
//  通过属性或其它成员提供对自定义字段的访问
         public   string  StringInfo
        {
            
get  {  return  stringInfo; }
        }

        
public   bool  BooleanInfo
        {
            
get  {  return  booleanInfo; }
        }

        
//  重写GetObjectData方法。
        
//  如果添加了自定义字段,一定要重写基类GetObjectData方法的实现。
         public   override   void  GetObjectData(SerializationInfo info, StreamingContext context)
        {
            
//  序列化自定义数据成员
            info.AddValue( " StringInfo " , stringInfo);
            info.AddValue(
" BooleanInfo " , booleanInfo);

            
//  调用基类方法,序列化它的成员
             base .GetObjectData(info, context);
        }

        
public   override   string  Message
        {
            
get
            {
                
string  message  =   base .Message;

                
if  (stringInfo  !=   null )
                {
                    message 
+=  Environment.NewLine  +
                        stringInfo 
+   "  =  "   +  booleanInfo;
                }

                
return  message;
            }
        }

    }


然后使用下面这样的代码测试异常的序列化:

     //  创建一个CustomException对象,将其序列化
    CustomException e  =   new  CustomException( " New Custom Exception " " My String Info " true );
    FileStream fs 
=   new  FileStream( @" Test " , FileMode.Create);
    IFormatter f 
=   new  SoapFormatter();
    f.Serialize(fs, e);
    fs.Close();

    
//  反序列化CustomException对象,查看它的字段
    fs  =   new  FileStream( @" Test " , FileMode.Open);
    e 
=  (f.Deserialize(fs))  as  CustomException;
    fs.Close();

    Console.WriteLine(e.Message);


此处需要引用System.Runtime.Serialization.Formatters.Soap.dll。

6、正确地使用异常

根据第2节的讨论,我们已经了解何种情况可以称之为异常。下面再给出一些异常使用相关的规范或建议。

借鉴《.NET设计规范》一书的做法,本节的这些规范和建议通常由要、考虑、避免、不要这些词进行组织,每一条都描述了一种好的或是不好的做法。对于好的做法,标以√,相应的,对那些不好的做法则标以×。而不同的措辞也能够揭示该条规范的重要性。

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

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

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

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

6.1 抛出异常

在设计我们自己的方法时,应考虑如何正确地抛出异常。

× 不要返回错误码。

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

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

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

考虑通过调用System.Environment.FailFastNew 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.ExceptionSystem.SystemException)时,把异常吞了。

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

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

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

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

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

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

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

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

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

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

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

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

 

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

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

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

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

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

6.3 标准异常类型的使用

 

× 不要抛出ExceptionSystemException类型的异常。

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

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

 

 

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

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

 

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

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

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

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

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


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

     public  FileAttributes Attributes
    {
        
set
        {
            
if  (value  ==   null )
            {
                
throw   new  ArgumentNullException( " value " );
            }
        }
    }


 

× 不要让公用的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模式来避免因异常而引发的性能问题,如果该方法在普通的场景中都可能会抛出异常。



参考:
 《.NET 框架程序设计》-Jeffery Richter
 《.NET 设计规范》-Krzysztof Cwalina, Brad Abrams
 《Visual C# 2005 Recipes》-Allen Jones, Matthew MacDonald
 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值