C#圖解教程 第二十二章 異常

異常

什麼是異常


異常是程序中的運行時錯誤,它違反了系統約束或應用程序約束,或出現了在正常操做時未預料的情形。例如,程序試圖除以0或試圖寫一個只讀文件。當這些發生時,系統捕獲這個錯誤並拋出(raise)一個異常。
若是程序沒有提供處理該異常的代碼,系統會掛起這個程序。例如,下面的代碼在試圖用0除一個數時拋出一個異常:ide

class Program
{
    static void Main()
    {
        int x=10,y=0;
        x/=y;        //用0除以一個數時拋出一個異常
    }
}

當這段代碼運行時,系統顯示下面的錯誤信息:
3d

try語句


try語句用來指明爲避免出現異常而被保護的代碼段,並在發生異常時提供代碼處理異常。try語句由3個部分組成,以下圖所示。code

  • try 塊包含爲避免出現異常而被保護的代碼
  • catch 子句部分含有一個或多個catch子句。這些是處理異常的代碼段,它們也稱爲是異常處理程序
  • finally 塊含有在全部狀況下都要被執行的代碼,不管有沒有異常發生

處理異常

前面的示例顯示了除以0會致使一個異常。能夠修改此程序,把那段代碼放在一個try塊中,並提供一個簡單的catch子句,以處理該異常。當異常發生時,它被捕獲並在catch塊中處理。對象

static void Main()
{
    int x = 10;
    try
    {
        int y=0;
        x/=y; //拋出異常
    }
    catch
    {
        …//異常處理代碼
        Console.WriteLine("Handling all exceptions - keep on Running");
    }
}

這段代碼產生如下消息。注意,除了輸出消息,沒有異常已經發生的跡象。
blog

異常類


有許多不一樣類型的異常能夠在程序中發生。BCL定義了許多類,每個類表明一個指定的異常類型。當一個異常發生時,CLR:排序

  • 建立該類型的異常對象
  • 尋找適當的catch子句以處理它

全部異常類都從根本上派生自System.Exception類。異常繼承層次的一個部分以下圖所示。

異常對象含有隻讀屬性,帶有致使該異常的信息。這些屬性的其中一些以下表所示。
繼承

catch 子句


catch子句處理異常。它有3種形式,容許不一樣級別的處理。這些形式以下圖所示。

通常catch子句能接受任何異常,但不能肯定引起異常的類型。這隻容許對任何可能發生的異常的普通處理和清理。
特定catch子句形式把一個異常類的名稱做爲參數。它匹配該指定類或派生自它的異常類的異常。
帶對象的特定catch子句提供關於異常的最多信息。它匹配該指定類的異常,或派生自它的異常類的異常。它還給出一個異常實例(稱爲異常變量),是一個對CLR建立的異常對象的引用。能夠在catch子句塊內部訪問異常變量的屬性,以獲取關於引發的異常的詳細信息。
例如,下面的代碼處理IndexOutOfRangeException類型的異常。當異常發生時,一個實際異常對象的引用被參數名e傳入代碼。那3個WriteLine語句中,每一個都從異常對象中讀取一個字符串字段。字符串

catch(IndexOutOfRangeException e)
{
    Console.WriteLine("Message: {0}",e.Message);
    Console.WriteLine("Source:  {0}",e.Source);
    Console.WriteLine("Stack:   {0}",e.StackTrace);
}

使用特定catch子句的示例


回到除以0的示例,下面的代碼把前面的catch子句修改成指定處理DivideByZeroException類的異常。在前面的示例中,catch子句會處理所在try塊中引發的任何異常,而這個示例將只處理DivideByZeroException類的異常。string

int x=10;
try
{
    int y=0;
    x/=y;
}
catch(DivideByZeroException)
{
    …
    Console.WriteLine("Handling an exception.");
}

能夠進一步修改catch子句以使用一個異常變量。這容許在catch塊內部訪問異常對象。it

int x=10;
try
{
    int y=0;
    x/=y;
}
catch(DivideByZeroException e)
{
    Console.WriteLine("Message: {0}",e.Message);
    Console.WriteLine("Source:  {0}",e.Source);
    Console.WriteLine("Stack:   {0}",e.StackTrace);
}

在筆者的電腦上,這段代碼會產生如下輸出。對於讀者的機器,第三行和第四行的代碼路徑可能不一樣,這要與你的項目位置和解決方案目錄匹配。

catch子句段


catch子句的目的是容許你以一種優雅的方式處理異常。若是你的catch子句接受一個參數,那麼系統會把這個異常變量設置爲異常對象,這樣你就能夠檢査並肯定異常的緣由。若是異常是前一個異常引發的,你能夠經過異常變量的InnerException屬性來得到對前一個異常對象的引用。catch子句段能夠包含多個catch子句。下圖顯示了catch子句段。
當異常發生時,系統按順序搜索catch子句的列表,第一個匹配該異常對象類型的catch子句被執行。所以,catch子句的排序有兩個重要的規則。具體以下。

  • 特定catch子句必須以一種順序排列,最明確的異常類型第一,直到最普通的類型。例如,若是聲明瞭一個派生自NullReferenceException的異常類,那麼派生異常類型的catch子句應該被列在NullReferenceException的catch子句以前
  • 若是有一個通常catch子句,它必須是最後一個,而且在全部特定catch子句以後。不鼓勵使用通常catch子句.由於它容許程序繼續執行隱藏錯誤,讓程序處於一種未知的狀態。應儘量使用特定catch子句

finally塊


若是程序的控制流進人了一個帶finally塊的try語句,那麼finally始終會被執行。下圖闡明瞭它的控制流。

  • 若是在try塊內部沒有異常發生,那麼在try塊的結尾,控制流跳過任何catch子句併到finally塊
  • 若是在try塊內部發生了異常,那麼在catch子句段中不管哪個適當的catch子句被執行,接着就是finally塊的執行


即便try塊中有return語句或在catch塊中拋出一個異常,finally塊也老是會在返回到調用代碼以前執行。例如,在下面的代碼中,在try塊的中間有一條return語句,它在某條件下被執行。
這不會使它繞過finally語句。

try
{
    if(inVal<10)
    {
        Console.Write("First Branch - ");
        return;
    }
    else
    {
        Console.Write("Second Branch - ");
    }
}
finally
{
    Console.WriteLine("In finally statement");
}

這段代碼在inVal值爲5時產生如下輸出:

爲異常尋找處理程序


當程序產生一個異常時,系統查看該程序是否爲它提供了一個處理代碼。下圖闡明瞭這個控制流。

  • 若是在try塊內發生了異常,系統會査看是否有任何一個catch子句能處理該異常
  • 若是找到了適當的catch子句,如下3項中的1項會發生
    • 該catch子句被執行
    • 若是有finally塊,那麼它被執行
    • 執行在try語句的尾部繼續(也就是說,在finally塊以後,或若是沒有finally塊,就在最後一個catch子句以後)

更進一步搜索


若是異常在一個沒有被try語句保護的代碼段中產生,或若是try語句沒有匹配的異常處理程序,系統將不得不更進一步尋找匹配的處理代碼。爲此它會按順序搜索調用棧,以看看是否存在帶匹配的處理程序的封裝try塊。
下圖闡明瞭這個搜索過程。圖左邊是代碼的調用結構,右邊是調用棧。該圖顯示Method2被從Method1的try塊內部調用。若是異常發生在Method2內的try塊內部,系統會執行如下操做。

  • 首先,它査看Method2是否有能處理該異常的異常處理程序
    • 若是有,Method2處理它,程序繼續執行
    • 若是沒有,系統再延着調用棧找到Method1,搜尋一個適當的處理程序
  • 若是Method1有一個適當的catch子句,那麼系統將:
    • 回到棧頂,那裏是Method2
    • 執行Method2的finally塊,並把Method2彈出棧
    • 執行Method1的catch子句和它的finally塊
  • 若是Method1沒有適當的catch子句,系統會繼續搜索調用棧。

通常法則

下圖展現了處理異常的通常法則。

搜索調用棧的示例

在下面的代碼中,Main開始執行並調用方法A,A調用方法B。代碼以後給出了相應的說明, 並在圖22-9中再現了整個過程。

class Program
{
    static void Main()
    {
        var MCls=new MyClass();
        try
        {
            MCls.A();
        }
        catch(DivideByZeroException e)
        {
            Console.WriteLine("catch clause in Main()");
        }
        finally
        {
            Console.WriteLine("finally clause in Main()");
        }
        Console.WriteLine("After try statement in Main.");
        Console.WriteLine("          -- keep running.");
    }
}
class MyClass
{
    public void A()
    {
        try
        {
            B();
        }
        catch(System.NullReferenceException)
        {
            Console.WriteLine("catch clause in A()");
        }
        finally
        {
            Console.WriteLine("finally clause in A()");
        }
    }
    void B()
    {
        int x=10,y=0;
        try
        {
            x/=y;
        }
        catch(System.IndexOutOfRangeException)
        {
            Console.WriteLine("catch clause in B()");
        }
        finally
        {
            Console.WriteLine("finally clause in B()");
        }
    }
}

這段代碼產生如下輸出:

  1. Main調用A,A調用B,B遇到一個DivideByZeroException異常
  2. 系統檢查B的catch段尋找匹配的catch子句。雖然它有一個IndexOutOfRangeException的子句,但沒有DivideByZeroException的
  3. 系統而後延着調用棧向下移動並檢査A的catch段,在那裏它發現A也沒有匹配的catch子句
  4. 系統繼續延調用棧向下,並檢查Main的catch子句部分,在那裏它發現Main確實有一個DivideByZeroException的catch子句
  5. 儘管匹配的catch子句如今被定位了,但並不執行。相反,系統回到棧的頂端,執行B的finally子句,並把B從調用棧中彈出
  6. 系統移動到A,執行它的finally子句,並把A從調用棧中彈出
  7. 最後,Main的匹配catch子句被執行,接着是它的finally子句。而後執行在Main的try語句結尾以後繼續

拋出異常


可使用throw語句使代碼顯式地引起一個異常。throw語句的語法以下:
throw ExceptionObject;
例如,下面的代碼定義了一個名稱爲PrintArg的方法,它帶一個string參數並把它打印出來。在try塊內部,它首先作檢査以確認該參數不是null。若是是null,它建立一個ArgumentNullException實例並拋出它。該異常實例在catch語句中被捕獲,而且該出錯消息被打印。Main調用該方法兩次:一次用null參數,而後用一個有效參數。

class MyClass
{
    public static void PrintArg(string arg)
    {
        try
        {
            if(arg==null)
            {
                var myEx=new ArgumentNullException("arg");
                throw myEx;
            }
            Console.WriteLine(arg);
        }
        catch(ArgumentNullException e)
        {
            Console.WriteLine("Message:  {0}",e.Message);
        }
    }
}
class Program
{
    static void Main()
    {
        string s=null;
        MyClass.PrintArg(s);
        MyClass.PrintArg("Hi there!");
    }
}

這段代碼產生如下輸出:

不帶異常對象的拋出


throw語句還能夠不帶異常對象使用,在catch塊內部。

  • 這種形式從新拋出當前異常,系統繼續它的搜索,爲該異常尋找另外的處理代碼
  • 這種形式只能用在catch語句內部

例如,下面的代碼從第一個catch子句內部從新拋出異常:

class MyClass
{
    public static void PrintArg(string arg)
    {
        try
        {
            try
            {
                if(arg==null)
                {
                    var myEx=new ArgumentNullException("arg");
                    throw myEx;
                }
                Console.WriteLine(arg);
            }
            catch(ArgumentNullException e)
            {
                Console.WriteLine("Inner Catch:  {0}",e.Message);
                throw;
            }
        }
        catch
        {
            Console.WriteLine("Outer Catch:  Handling an Exception.");
        }
    }
}
class Program
{
    static void Main()
    {
        string s=null;
        MyClass.PrintArg(s);
    }
}

這段代碼產生如下輸出:

<wiz_tmp_tag id="wiz-table-range-border" contenteditable="false" style="display: none;">

相關文章
相關標籤/搜索