c++異常處理 (轉)

worldblog發表於2008-01-22
c++異常處理 (轉)[@more@]第七章 異常處理
通用語言執行時(CLR)具有的一個很大的優勢為,異常處理是跨語言被標準化的。一個在中所引發的異常可以在Visual Basic客戶中得到處理。不再有 HRESULTs 或者 ISupportErrorInfo 介面。
儘管跨語言異常處理的覆蓋面很廣,但這一章完全集中討論C#異常處理。你稍為改變的處理行為,接著有趣的事情就開始了:你處理了該異常。要增加更多的手段,隨後引發你所建立的異常。

7.1 校驗(checked)和非校驗(unchecked)語句
當你運算時,有可能會發生計算結果超出結果變數資料型別的有效範圍。這種情況被稱為溢位,依據不同的語言,你將被以某種方式通知——或者根本就沒有被通知。(C++員聽起來熟悉嗎?)
那麼,C#如何處理溢位的呢? 要找出其預設行為,請看我在這本書前面提到的階乘的例子。(為了方便其見,前面的例子再次在清單 7.1 中給出)

清單 7.1 計算一個數的階乘

1: using System;
2: 
3: class Factorial
4: {
5: public static void Main(string[] args)
6: {
7: long nFactorial = 1;
8: long nComputeTo = Int64.Parse(args[0]);
9: 
10: long nCurDig = 1;
11: for (nCurDig=1;nCurDig <= nComputeTo; nCurDig++)
12: nFactorial *= nCurDig;
13: 
14: Console.WriteLine("{0}! is {1}",nComputeTo, nFactorial);
15: }
16: }

當你象這樣使用命令列執行程式時
factorial 2000

結果為0,什麼也沒有發生。因此,設想C#默默地處理溢位情況而不明確地警告你是的。
透過給整個應用程式(經編譯器開關)或於語句級允許溢位校驗,你就可以改變這種行為。以下兩節分別解決一種方案。
7.1.1 給溢位校驗設定編譯器
如果你想給整個應用程式控制溢位校驗,C#編譯器設定選擇是正是你所要找的。預設地,溢位校驗是禁用的。要明確地要求它,執行以下編譯器命令:
csc factorial.cs /checked+

現在當你用2000引數執行應用程式時,CLR通知你溢位異常(見圖 7.1)。

圖 7.1 允許了溢位異常,階乘程式碼產生了一個異常。

  按OK鍵離開對話方塊揭示了異常資訊:
Exception occurred: System.OverflowException
at Factorial.Main(System.String[])

  現在你瞭解了溢位條件引發了一個 System.OverflowException異常。下一節,在我們完成語法校驗之後,如何捕獲並處理所出現的異常?
7.1.2 語法溢位校驗
  如果你不想給整個應用程式允許溢位校驗,僅給某些程式碼段允許校驗,你可能會很舒適。對於這種場合,你可能象清單7.2中顯示的那樣,使用校驗語句。

清單 7.2  階乘計算中的溢位校驗

1: using System;
2: 
3: class Factorial
4: {
5: public static void Main(string[] args)
6: {
7: long nFactorial = 1;
8: long nComputeTo = Int64.Parse(args[0]);
9: 
10: long nCurDig = 1;
11: 
12: for (nCurDig=1;nCurDig <= nComputeTo; nCurDig++)
13: checked { nFactorial *= nCurDig; }
14: 
15: Console.WriteLine("{0}! is {1}",nComputeTo, nFactorial);
16: }
17: }

  甚至就如你運用標誌 checked-編譯了該程式碼,在第13行中,溢位校驗仍然會對乘法實現檢查。錯誤資訊保持一致。

  顯示相反行為的語句是非校驗(unchecked )。甚至如果允許了溢位校驗(給編譯器加上checked+標誌),被unchecked 語句所括住的程式碼也將不會引發溢位異常:

unchecked
{
nFactorial *= nCurDig;
}

7.2  異常處理語句
  既然你知道了如何產生一個異常(你會發現更多的方法,相信我),仍然存在如何處理它的問題。如果你是一個 C++  程式設計師,肯定熟悉SEH(結構異常處理)。你將從中找到安慰,C#中的命令幾乎是相同的,而且它們也以相似的方式運作。

The following three sections introduce C#'s exception-handling statements:
以下三節介紹了C#的異常處理語句:

。用 try-catch 捕獲異常
。用try-finally 清除異常
。用try-catch-finally 處理所有的異常

7.2.1  使用 try 和 catch捕獲異常
  你肯定會對一件事非常感興趣——不要提示給那令人討厭的異常訊息,以便你的應用程式繼續執行。要這樣,你必須捕獲(處理)該異常。
這樣使用的語句是try 和 catch。try包含可能會產生異常的語句,而catch處理一個異常,如果有異常存在的話。清單7.3 用try 和 catch為OverflowException 實現異常處理。

清單7.3 捕獲由Factorial Calculation引發的OverflowException 異常

1: using System;
2: 
3: class Factorial
4: {
5: public static void Main(string[] args)
6: {
7: long nFactorial = 1, nCurDig=1;
8: long nComputeTo = Int64.Parse(args[0]);
9: 
10: try
11: {
12: checked
13: {
14: for (;nCurDig <= nComputeTo; nCurDig++)
15: nFactorial *= nCurDig;
16: }
17: }
18: catch (OverflowException oe)
19: {
20: Console.WriteLine("Computing {0} caused an overflow exception", nComputeTo);
21: return;
22: }
23: 
24: Console.WriteLine("{0}! is {1}",nComputeTo, nFactorial);
25: }
26: }

為了說明清楚,我擴充套件了某些程式碼段,而且我也保證異常是由checked 語句產生的,甚至當你忘記了編譯器設定時。
正如你所見,異常處理並不麻煩。你所有要做的是:在try語句中包含容易產生異常的程式碼,接著捕獲異常,該異常在這個例子中是OverflowException型別。無論一個異常什麼時候被引發,在catch段裡的程式碼會注意進行適當的處理。
如果你不事先知道哪一種異常會被預期,而仍然想處於安全狀態,簡單地忽略異常的型別。

try
{
...
}
catch
{
...
}

但是,透過這個途徑,你不能獲得對異常的訪問,而該物件含有重要的出錯資訊。一般化異常處理程式碼象這樣:

try
{
...
}
catch(System.Exception e)
{
...
}

注意,你不能用ref或out 修飾符傳遞 e 物件給一個方法,也不能賦給它一個不同的值。

7.2.2 使用 try 和 finally 清除異常
如果你更關心清除而不是錯誤處理, try 和 finally 會獲得你的喜歡。它不僅抑制了出錯訊息,而且所有包含在 finally 塊中的程式碼在異常被引發後仍然會被執行。
儘管程式不正常終止,但你還可以為使用者獲取一條訊息,如清單 7.4 所示。

清單 7.4 在finally 語句中處理異常

1: using System;
2: 
3: class Factorial
4: {
5: public static void Main(string[] args)
6: {
7: long nFactorial = 1, nCurDig=1;
8: long nComputeTo = Int64.Parse(args[0]);
9: bool bAllFine = false;
10: 
11: try
12: {
13: checked
14: {
15: for (;nCurDig <= nComputeTo; nCurDig++)
16: nFactorial *= nCurDig;
17: }
18: bAllFine = true;
19: }
20: finally
21: {
22: if (!bAllFine)
23: Console.WriteLine("Computing {0} caused an overflow exception", nComputeTo);
24: else
25: Console.WriteLine("{0}! is {1}",nComputeTo, nFactorial);
26: }
27: }
28: }

透過檢測該程式碼,你可能會猜到,即使沒有引發異常處理,finally也會被執行。這是真的——在finally中的程式碼總是會被執行的,不管是否具有異常條件。為了舉例說明如何在兩種情況下提供一些有意義的資訊給使用者, 我引進了新變數bAllFine。bAllFine告訴finally 語段,它是否是因為一個異常或者僅是因為計算的順利完成而被。
作為一個習慣了SEH程式設計師,你可能會想,是否有一個與__leave 語句等價的語句,該語句在C++中很管用。如果你還不瞭解,在C++中的__leave 語句是用來提前終止 try 語段中的執行程式碼,並立即跳轉到finally 語段 。
壞訊息, C# 中沒有__leave 語句。但是,在清單 7.5 中的程式碼演示了一個你可以實現的方案。

清單 7.5 從 try語句 跳轉到finally 語句

1: using System;
2: 
3: class JumpTest
4: {
5: public static void Main()
6: {
7: try
8: {
9: Console.WriteLine("try");
10: goto __leave;
11: }
12: finally
13: {
14: Console.WriteLine("finally");
15: }
16: 
17: __leave:
18: Console.WriteLine("__leave");
19: }
20: }


當這個應用程式執行時,輸出結果為

try
finally
__leave

一個 goto 語句不能退出 一個finally 語段。甚至把 goto 語句放在 try 語句 段中,還是會立即返回控制到 finally 語段。因此,goto 只是離開了 try 語段並跳轉到finally 語段。直到 finally 中的程式碼完成執行後,才能到達__leave 標籤。按這種方式,你可以模仿在SEH中使用的的__leave 語句。
順便地,你可能懷疑goto 語句被忽略了,因為它是try 語句中的最後一條語句,並且控制自動地轉移到了 finally 。為了證明不是這樣,試把goto 語句放到Console.WriteLine 方法呼叫之前。儘管由於不可到達程式碼你得到了編譯器的警告,但是你將看到goto語句實際上被執行了,且沒有為 try 字串產生的輸出。

7.2.3 使用try-catch-finally處理所有異常
應用程式最有可能的途徑是合併前面兩種錯誤處理技術——捕獲錯誤、清除並繼續執行應用程式。所有你要做的是在出錯處理程式碼中使用 try 、catch 和 finally語句。清單 7.6 顯示了處理零除錯誤的途徑。

清單 7.6 實現多個catch 語句

1: using System;
2: 
3: class CatchIT
4: {
5: public static void Main()
6: {
7: try
8: {
9: int nTheZero = 0;
10: int nResult = 10 / nTheZero;
11: }
12: catch(DivByZeroException divEx)
13: {
14: Console.WriteLine("divide by zero occurred!");
15: }
16: catch(Exception Ex)
17: {
18: Console.WriteLine("some other exception");
19: }
20: finally
21: {
22: }
23: }
24: }

這個例子的技巧為,它包含了多個catch 語句。第一個捕獲了更可能出現的DivideByZeroException異常,而第二個catch語句透過捕獲普通異常處理了所有剩下來的異常。
你肯定總是首先捕獲特定的異常,接著是普通的異常。如果你不按這個順序捕獲異常,會發生什麼事呢?清單7.7中的程式碼有說明。

清單7.7 順序不適當的 catch 語句

1: try
2: {
3: int nTheZero = 0;
4: int nResult = 10 / nTheZero;
5: }
6: catch(Exception Ex)
7: {
8: Console.WriteLine("exception " + Ex.ToString());
9: }
10: catch(DivideByZeroException divEx)
11: {
12: Console.WriteLine("never going to see that");
13: }


編譯器將捕獲到一個小錯誤,並類似這樣報告該錯誤:
wrongcatch.cs(10,9): error CS0160: A previous catch clause already 
catches all exceptions of this or a super type ('System.Exception')

最後,我必須告發CLR異常與SEH相比時的一個缺點(或差別):沒有 EXCEPTION_CONTINUE_EXECUTION識別符號的等價物,它在SEH異常過濾器中很有用。基本上,EXCEPTION_CONTINUE_EXECUTION 允許你重新執行負責異常的程式碼片段。在重新執行之前,你有機會更改變數等。我個人特別喜歡的技術為,使用訪問違例異常,按需要實施分配。


7.3 引發異常
當你必須捕獲異常時,其他人首先必須首先能夠引發異常。而且,不僅其他人能夠引發,你也可以負責引發。其相當簡單:

throw new ArgumentException("Argument can't be 5");
你所需要的是throw 語句和一個適當的異常類。我已經從表7.1提供的清單中選出一個異常給這個例子。

表 7.1 Runtime提供的標準異常


異常型別 描述

Exception 所有異常物件的基類
SystemException 執行時產生的所有錯誤的基類
IndexOutOfRangeException 當一個陣列的下標超出範圍時執行時引發
NullReferenceException 當一個空物件被引用時執行時引發
InvalidOperationException 當對方法的呼叫對物件的當前狀態無效時,由某些方法引發
ArgumentException 所有引數異常的基類
ArgumentNullException 在引數為空(不允許)的情況下,由方法引發
ArgumentOutOfRangeException 當引數不在一個給定範圍之內時,由方法引發
InteropException 目標在或發生在CLR外面環境中的異常的基類
ComException 包含COM 類的HRESULT資訊的異常
SEHException 封裝win32 結構異常處理資訊的異常

然而,在catch語句的內部,你已經有了隨意處置的異常,就不必建立一個新異常。可能在表7.1 中的異常沒有一個符合你特殊的要求——為什麼不建立一個新的異常?在即將要學到小節中,都涉及到這兩個話題。

7.3.1 重新引發異常
當處於一個catch 語句的內部時,你可能決定引發一個目前正在再度處理的異常,留下進一步的處理給一些外部的try-catch 語句。該方法的例子如 清單7.8所示。

清單 7.8 重新引發一個異常

1: try
2: {
3: checked
4: {
5: for (;nCurDig <= nComputeTo; nCurDig++)
6: nFactorial *= nCurDig;
7: }
8: }
9: catch (OverflowException oe)
10: {
11: Console.WriteLine("Computing {0} caused an overflow exception", nComputeTo);
12: throw;
13: }

注意,我不必規定所宣告的異常變數。儘管它是可選的,但你也可以這樣寫:
throw oe;
現在有時還必須留意這個異常。

7.3.2 建立自己的異常類
儘管建議使用預定義的異常類,但對於實際場合,建立自己的異常類可能會方便。建立自己的異常類,允許你的異常類的使用者根據該異常類採取不同的手段。
在清單 7.9 中出現的異常類 MyImportantException遵循兩個規則:第一,它用Exception結束類名。第二,它實現了所有三個被推薦的通用結構。你也應該遵守這些規則。
清單 7.9 實現自己的異常類 MyImportantException

1: using System;
2: 
3: public class MyImportantException:Exception
4: {
5: public MyImportantException()
6: :base() {}
7: 
8: public MyImportantException(string message)
9: :base(message) {}
10: 
11: public MyImportantException(string message, Exception inner)
12: :base(message,inner) {}
13: }
14: 
15: public class ExceptionTestApp
16: {
17: public static void TestThrow()
18: {
19: throw new MyImportantException("something bad has happened.");
20: }
21: 
22: public static void Main()
23: {
24: try
25: {
26: ExceptionTestApp.TestThrow();
27: }
28: catch (Exception e)
29: {
30: Console.WriteLine(e);
31: }
32: }
33: }

正如你所看到的,MyImportantException 異常類不能實現任何特殊的功能,但它完全基於System.Exception類。程式的剩餘部分測試新的異常類,給System.Exception 類使用一個catch 語句。
如果沒有特殊的實現而只是給MyImportantException定義了三個構造,建立它又有什麼意義呢?它是一個重要的型別——你可以在catch語句中使用它,代替更為普通的異常類。可能引發你的新異常的客戶程式碼可以按規定的catch程式碼發揮作用。
當使用自己的名字空間編寫一個類庫時,也要把異常放到該名字空間。儘管它並沒有出現在這個例子中,你還是應該使用適當的屬性,為擴充套件了的錯誤資訊擴充你的異常類。

7.4 異常處理的“要”和“不要” 
作為最後的忠告之語,這裡是對異常引發和處理所要做和不要做的清單:
。當引發異常時,要提供有意義的文字。
。要引發異常僅當條件是真正異常;也就是當一個正常的返回值不滿足時。
。如果你的方法或屬性被傳遞一個壞引數,要引發一個ArgumentException異常。
。當呼叫操作不適合物件的當前狀態時,要引發一個 InvalidOperationException異常。
。要引發最適合的異常。
。要使用連結異常,它們允許你跟蹤異常樹。
。不要為正常或預期的錯誤使用異常。
。不要為流程的正常控制使用異常。
。不要在方法中引發 NullReferenceException或IndexOutOfRangeException異常。

7.5 小結
這一章由介紹溢位校驗開始。你可以使用編譯器開關(預設是關),使整個應用程式允許或禁止溢位校驗。如果需要微調控制,你可以使用校驗和非校驗語句,它允許你使用或不使用溢位校驗來執行一段程式碼,儘管沒有給應用程式設定開關。
當發生溢位時,一個異常就被引發了。如何處理異常取決於你。我提出了各種途徑,包括你最有可能貫穿整個應用程式使用的:try、catch 和finally 語句。在伴隨的多個例子中,你學到了它與WIN32結構異常處理(SEH)的差別。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-998125/,如需轉載,請註明出處,否則將追究法律責任。

相關文章