LYWILL设计运营 - 网站运营与推广、开发技术、成功项目展示。

深入.NET异常机制

 

 

       在.NET开发中异常处理用try..catch...finally,将可能发生异常的代码放置在try里面,如果发生异常用catch(Exception ex)可以一网打尽,然后finally可以做些请求的资源的清理工作,不过这种方法很低效。

       我们实现自己的类时,通常会为自己的类提供一些接口(如方法,事件,属性等)以供调用者使用,如果调用者调用接口无法完成接口预定功能时,我们就必须通知调用者接口调用出错,这时候我们通常有两种方式来通知调用者。第一种是返回错误码的方式,如接口预期完成实现返回0,没有预期完成实现返回1;第二种可以使用.NET Framework提供的异常处理机制,在我们的接口没有完成预期功能的时候抛出一个派生自System.Exception类的实例,调用者可以通过捕捉这个实例进行错误处理。

 

      必须了解的System.Exception类

        CLS(公共语言规范)硬性规定所有面向CLR的编程语言都必须支持抛出从System.Exception派生的对象,C#编译器只允许代码派出从Exception派生的对象,所以我们用C#编写程序,毫无疑问,我们自定义的异常类必须派生自 System.Exception,否则你无法通过throw将它抛出来。所以我们在使用自定义异常之前,必须对System.Exception类有更多的了解。

 

       System.Exception的公共方法中,除了GetBaseException(用以遍历所以内部异常组成的链表,返回最开始那个异常),其它都没有特别的,我们更多利用的是它提供个属性。

 

 

属性

访问权限

类型

描述

Message

只读

String

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

Data

只读

IDictionary

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

Source

读写

String

产生异常的程序集的名称

StackTrace

只读

String

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

TargetSite

只读

MethodBase

抛出异常的方法。

HelpLink

读写

String

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

InnerExceptoin

只读

Exception

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

 

      我们主要通过Exception的属性获取代码出错信息的描述和出错的具体位置,以方便我们快速定位BUG和进行修复。所以我平时用到最多有两个属性,一个是Message,另外一个是StackTrace。这两个属性类型都是string,通常我们将Message写入Log日志文件,TargetSite获取的是抛出异常的方法名称,而异常发生时访问StackTrace属性可以获取一个堆栈跟踪,这个堆栈跟踪描述了异常发生前调用的方法,这些信息对于我们检查异常原因和修复代码都是非常有帮助的。

 

      异常的处理机制

 

      下面的代码展示了异常处理机制的标准用法

 

public static void SomeMethod() {
      
try {
         
// 此次编写可能发生异常的代码
      }
      
catch (InvalidOperationException) {
         
// 从异常InvalidOperationException恢复的代码...
      }
      
catch (IOException) {
         
// 从IOException恢复的代码...
      }
      
catch (Exception) {
         
//  C# 2.0之前, 这里的catch仅能捕捉到和CLS相容的异常
         
// 在C# 2.0版本中以及之后, 这里的catch能捕捉到和CLS兼容和非兼容的异常 因为不兼容CLS的异常会被包装成一个RuntimeWrappedException类型的对象抛出
         throw; 重新抛出异常
      }
      
catch {
         
// 所有C#版本中, 这里的catch能捕捉到和CLS兼容和不兼容的异常
         throw// 重新抛出异常
      }
      
finally {
         
// 对try的任何操作进行清理
        
      }
      
// 如果try没有抛出异常或者某个catch块捕捉到异常没有抛出或重新抛出,都会执行下finally下面的代码
     
   }

 

      try块包含可能会抛出异常的代码,catch块一般是对异常进行恢复和错误的记录,如对数据库进行批量操作时,如果发生异常,可以在catch块里面执行回滚的代码,finally块是对try的任何操作进行清理,只要异常被catch住了,finally块代码就一定会被执行。一个try块至少有一个关联的catch块或finally块。如果try块中代码没有任何异常,线程会跳过与之关联的所有的catch块,如果有finally就直接执行finally块代码,完毕后从finally后面代码继续执行。前面说过C#里面所有自定制的异常都必须派生自Exception类,所以这里catch的捕捉类型就必须是System.Exception或者是它的派生类型。因为一个try块可以对应多个catch块,而CLR是自上而下搜索一个匹配的catch块的,所以我们应该将派生程度最大也就是最具体的异常类型放在最前面的catch的捕捉类型里。而CLR一旦找到匹配的catch块,后面的catch块就不会搜索了。如果该try块相关联的catch中没有一个能够接受该异常,CLR将沿着调用堆栈向更高层搜索能够接受该异常的catch块,如果直到堆栈顶部依然没有找到能够处理该异常的catch块,就会发生一个未处理的异常,CLR就会终止进程

 

      当CLR找到一个具有匹配捕捉异常类型的catch时,它会先执行从抛出异常的try块开始,到匹配异常的catch块为止的范围内所有的finally块,然后才执行这个catch块里面的代码,完后执行者catch对应finally的代码。理解这一句很重要,下面我们通过一段代码来说明这个执行的过程:

 

namespace ExceptionDemo
{
    
class Program
    {
        
static void Main(string[] args)
        {
            
try
            {
                
double result = Calculation.DividedBy(00);
                Console.WriteLine(result);
               
            }
            
catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            
finally
            {
                Console.WriteLine(
"外层finally");
            }
           
            Console.ReadKey();
        }

    }
    
/// <summary>
    
/// 计算类
    
/// </summary>
    public static class Calculation
    {
        
/// <summary>
        
/// 除法
        
/// </summary>
        public static double DividedBy(int a, int b)
        {
            
double result = 0.0;
            
try
            {
                result 
= a / b;
            }
            
catch (System.NotFiniteNumberException ex)
            {
                
throw;
            }
            
finally
            {
                Console.WriteLine(
"内层finally");
            }
            
return result;
        }
    }
}

 

      这段代码执行的结果如下:

      内层finally
      尝试除以零。
      外层finally

 

      具体的执行过程是这样,当程序主函数Main执行Calculation.DividedBy(00)方法时,在DividedBy方法内部执行语句result = a / b时会抛出一个 System.DivideByZeroException类型的异常,这个时候在DividedBy方法的没有找到和DivideByZeroException匹配的捕捉类型,CLR会去调用栈的更高一层搜索与异常匹配的捕捉类型,这样CLR就会搜索到Main函数里面的catch(Exception ex),找到这个具有匹配的捕捉类型的catch后,线程就会开始执行DividedBy方法内部的那个finally块,然后执行Main函数那个匹配的catch块内容,再然后执行Main函数里面那个finally块。
 

       在catch的圆括号中我们除了可以指定异常的捕捉类型外,还可以指定一个异常变量,如ex,当该catch块匹配异常时,会把该异常对象的指引地址赋给这个变量。在catch块的末尾,我们有三种选择:

      1)重新抛出相同的异常,用throw或者throw ex(ex为catch圆括号指定的变量名);

      2)抛出一个不同的异常,这个异常可以是自定义的也可以是FCL中定义的;

      3)让线程从catch块底部退出,继续执行后面的代码。

 

      上面谈到throw和throw ex,这两种方式都是重新抛出相同异常,但有什么区别呢?

 

      一个异常抛出的时候,CLR会记录throw指令的位置(抛出位置)。一个catch块捕捉到该异常时,CLR又会记录异常的捕捉位置,在catch块内访问抛出的异常对象的StackTrace属性可以查看异常抛出位置到异常捕捉位置之间的所有方法。它们的区别就是CLR对于异常抛出起始位置的认知,当你用throw ex的时候会让CLR认为这里是异常抛出位置的起点,而throw则不会改变CLR对异常抛出起始点的认知。拿上面DividedBy方法来说,如用throw ex则会让CLR认为这里是异常抛出的起点,如果用throw,CLR会认为result=a/b才是异常抛出的起点。不过比较奇怪的是,不管你是throw还是throw ex ,在调用栈高一层捕捉到这个异常时查看StackTrace的信息都是一样,记录的位置都是thow或throw ex的位置