第七章 異常處理
通用語言運(yùn)行時(CLR)具有的一個很大的優(yōu)勢為,異常處理是跨語言被標(biāo)準(zhǔn)化的。一個在C#中所引發(fā)的異常可以在 Visual Basic客戶中得到處理。不再有 HRESULTs 或者 ISupportErrorInfo 接口。 盡管跨語言異常處理的覆蓋面很廣,但這一章完全集中討論C#異常處理。你稍為改變編譯器的溢出處理行為,接著有 趣的事情就開始了:你處理了該異常。要增加更多的手段,隨后引發(fā)你所創(chuàng)建的異常。
7.1 校驗(checked)和非校驗(unchecked)語句 當(dāng)你執(zhí)行運(yùn)算時,有可能會發(fā)生計算結(jié)果超出結(jié)果變量數(shù)據(jù)類型的有效范圍。這種情況被稱為溢出,依據(jù)不同的編程 語言,你將被以某種方式通知——或者根本就沒有被通知。(C++程序員聽起來熟悉嗎?) 那么,C#如何處理溢出的呢? 要找出其默認(rèn)行為,請看我在這本書前面提到的階乘的例子。(為了方便其見,前面 的例子再次在清單 7.1 中給出)
清單 7.1 計算一個數(shù)的階乘
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: }
當(dāng)你象這樣使用命令行執(zhí)行程序時 factorial 2000
結(jié)果為0,什么也沒有發(fā)生。因此,設(shè)想C#默默地處理溢出情況而不明確地警告你是安全的。 通過給整個應(yīng)用程序(經(jīng)編譯器開關(guān))或于語句級允許溢出校驗,你就可以改變這種行為。以下兩節(jié)分別解決一種方 案。 7.1.1 給溢出校驗設(shè)置編譯器 如果你想給整個應(yīng)用程序控制溢出校驗,C#編譯器設(shè)置選擇是正是你所要找的。默認(rèn)地,溢出校驗是禁用的。要明確 地要求它,運(yùn)行以下編譯器命令: csc factorial.cs /checked+
現(xiàn)在當(dāng)你用2000參數(shù)執(zhí)行應(yīng)用程序時,CLR通知你溢出異常(見圖 7.1)。
圖 7.1 允許了溢出異常,階乘代碼產(chǎn)生了一個異常。
按OK鍵離開對話框揭示了異常信息: Exception occurred: System.OverflowException at Factorial.Main(System.String[])
現(xiàn)在你了解了溢出條件引發(fā)了一個 System.OverflowException異常。下一節(jié),在我們完成語法校驗之后,如何捕獲并 處理所出現(xiàn)的異常? 7.1.2 語法溢出校驗 如果你不想給整個應(yīng)用程序允許溢出校驗,僅給某些代碼段允許校驗,你可能會很舒適。對于這種場合,你可能象清 單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: }
甚至就如你運(yùn)用標(biāo)志 checked-編譯了該代碼,在第13行中,溢出校驗仍然會對乘法實(shí)現(xiàn)檢查。錯誤信息保持一致。
顯示相反行為的語句是非校驗(unchecked )。甚至如果允許了溢出校驗(給編譯器加上checked+標(biāo)志),被 unchecked 語句所括住的代碼也將不會引發(fā)溢出異常:
unchecked { nFactorial *= nCurDig; }
7.2 異常處理語句 既然你知道了如何產(chǎn)生一個異常(你會發(fā)現(xiàn)更多的方法,相信我),仍然存在如何處理它的問題。如果你是一個 C++ WIN32 程序員,肯定熟悉SEH(結(jié)構(gòu)異常處理)。你將從中找到安慰,C#中的命令幾乎是相同的,而且它們也以相似的方 式運(yùn)作。
The following three sections introduce C#'s exception-handling statements: 以下三節(jié)介紹了C#的異常處理語句:
。用 try-catch 捕獲異常 。用try-finally 清除異常 。用try-catch-finally 處理所有的異常
7.2.1 使用 try 和 catch捕獲異常 你肯定會對一件事非常感興趣——不要提示給用戶那令人討厭的異常消息,以便你的應(yīng)用程序繼續(xù)執(zhí)行。要這樣,你 必須捕獲(處理)該異常。 這樣使用的語句是try 和 catch。try包含可能會產(chǎn)生異常的語句,而catch處理一個異常,如果有異常存在的話。清 單7.3 用try 和 catch為OverflowException 實(shí)現(xiàn)異常處理。
清單7.3 捕獲由Factorial Calculation引發(fā)的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: }
為了說明清楚,我擴(kuò)展了某些代碼段,而且我也保證異常是由checked 語句產(chǎn)生的,甚至當(dāng)你忘記了編譯器設(shè)置時。 正如你所見,異常處理并不麻煩。你所有要做的是:在try語句中包含容易產(chǎn)生異常的代碼,接著捕獲異常,該異常在 這個例子中是OverflowException類型。無論一個異常什么時候被引發(fā),在catch段里的代碼會注意進(jìn)行適當(dāng)?shù)奶幚怼?br>如果你不事先知道哪一種異常會被預(yù)期,而仍然想處于安全狀態(tài),簡單地忽略異常的類型。
try { ... } catch { ... }
但是,通過這個途徑,你不能獲得對異常對象的訪問,而該對象含有重要的出錯信息。一般化異常處理代碼象這樣:
try { ... } catch(System.Exception e) { ... }
注意,你不能用ref或out 修飾符傳遞 e 對象給一個方法,也不能賦給它一個不同的值。
7.2.2 使用 try 和 finally 清除異常 如果你更關(guān)心清除而不是錯誤處理, try 和 finally 會獲得你的喜歡。它不僅抑制了出錯消息,而且所有包含在 finally 塊中的代碼在異常被引發(fā)后仍然會被執(zhí)行。 盡管程序不正常終止,但你還可以為用戶獲取一條消息,如清單 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: }
通過檢測該代碼,你可能會猜到,即使沒有引發(fā)異常處理,finally也會被執(zhí)行。這是真的——在finally中的代碼總 是會被執(zhí)行的,不管是否具有異常條件。為了舉例說明如何在兩種情況下提供一些有意義的信息給用戶, 我引進(jìn)了新變量 bAllFine。bAllFine告訴finally 語段,它是否是因為一個異常或者僅是因為計算的順利完成而被調(diào)用。 作為一個習(xí)慣了SEH程序員,你可能會想,是否有一個與__leave 語句等價的語句,該語句在C++中很管用。如果你還 不了解,在C++中的__leave 語句是用來提前終止 try 語段中的執(zhí)行代碼,并立即跳轉(zhuǎn)到finally 語段 。 壞消息, C# 中沒有__leave 語句。但是,在清單 7.5 中的代碼演示了一個你可以實(shí)現(xiàn)的方案。
清單 7.5 從 try語句 跳轉(zhuǎn)到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: }
當(dāng)這個應(yīng)用程序運(yùn)行時,輸出結(jié)果為
try finally __leave
一個 goto 語句不能退出 一個finally 語段。甚至把 goto 語句放在 try 語句 段中,還是會立即返回控制到 finally 語段。因此,goto 只是離開了 try 語段并跳轉(zhuǎn)到finally 語段。直到 finally 中的代碼完成運(yùn)行后,才能到達(dá) __leave 標(biāo)簽。按這種方式,你可以模仿在SEH中使用的的__leave 語句。 順便地,你可能懷疑goto 語句被忽略了,因為它是try 語句中的最后一條語句,并且控制自動地轉(zhuǎn)移到了 finally 。為了證明不是這樣,試把goto 語句放到Console.WriteLine 方法調(diào)用之前。盡管由于不可到達(dá)代碼你得到了編 譯器的警告,但是你將看到goto語句實(shí)際上被執(zhí)行了,且沒有為 try 字符串產(chǎn)生的輸出。
7.2.3 使用try-catch-finally處理所有異常 應(yīng)用程序最有可能的途徑是合并前面兩種錯誤處理技術(shù)——捕獲錯誤、清除并繼續(xù)執(zhí)行應(yīng)用程序。所有你要做的是在 出錯處理代碼中使用 try 、catch 和 finally語句。清單 7.6 顯示了處理零除錯誤的途徑。
清單 7.6 實(shí)現(xiàn)多個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(DivideByZeroException 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 語句。第一個捕獲了更可能出現(xiàn)的DivideByZeroException異常,而第二個 catch語句通過捕獲普通異常處理了所有剩下來的異常。 你肯定總是首先捕獲特定的異常,接著是普通的異常。如果你不按這個順序捕獲異常,會發(fā)生什么事呢?清單7.7中的 代碼有說明。
清單7.7 順序不適當(dāng)?shù)?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')
最后,我必須告發(fā)CLR異常與SEH相比時的一個缺點(diǎn)(或差別):沒有 EXCEPTION_CONTINUE_EXECUTION標(biāo)識符的等價 物,它在SEH異常過濾器中很有用。基本上,EXCEPTION_CONTINUE_EXECUTION 允許你重新執(zhí)行負(fù)責(zé)異常的代碼片段。在重 新執(zhí)行之前,你有機(jī)會更改變量等。我個人特別喜歡的技術(shù)為,使用訪問違例異常,按需要實(shí)施內(nèi)存分配。
7.3 引發(fā)異常 當(dāng)你必須捕獲異常時,其他人首先必須首先能夠引發(fā)異常。而且,不僅其他人能夠引發(fā),你也可以負(fù)責(zé)引發(fā)。其相當(dāng) 簡單:
throw new ArgumentException("Argument can't be 5"); 你所需要的是throw 語句和一個適當(dāng)?shù)漠惓n悺N乙呀?jīng)從表7.1提供的清單中選出一個異常給這個例子。
表 7.1 Runtime提供的標(biāo)準(zhǔn)異常
異常類型 描述
Exception 所有異常對象的基類 SystemException 運(yùn)行時產(chǎn)生的所有錯誤的基類 IndexOutOfRangeException 當(dāng)一個數(shù)組的下標(biāo)超出范圍時運(yùn)行時引發(fā) NullReferenceException 當(dāng)一個空對象被引用時運(yùn)行時引發(fā) InvalidOperationException 當(dāng)對方法的調(diào)用對對象的當(dāng)前狀態(tài)無效時,由某些方法引發(fā) ArgumentException 所有參數(shù)異常的基類 ArgumentNullException 在參數(shù)為空(不允許)的情況下,由方法引發(fā) ArgumentOutOfRangeException 當(dāng)參數(shù)不在一個給定范圍之內(nèi)時,由方法引發(fā) InteropException 目標(biāo)在或發(fā)生在CLR外面環(huán)境中的異常的基類 ComException 包含COM 類的HRESULT信息的異常 SEHException 封裝win32 結(jié)構(gòu)異常處理信息的異常
然而,在catch語句的內(nèi)部,你已經(jīng)有了隨意處置的異常,就不必創(chuàng)建一個新異常。可能在表7.1 中的異常沒有一個符 合你特殊的要求——為什么不創(chuàng)建一個新的異常?在即將要學(xué)到小節(jié)中,都涉及到這兩個話題。
7.3.1 重新引發(fā)異常 當(dāng)處于一個catch 語句的內(nèi)部時,你可能決定引發(fā)一個目前正在再度處理的異常,留下進(jìn)一步的處理給一些外部的 try-catch 語句。該方法的例子如 清單7.8所示。
清單 7.8 重新引發(fā)一個異常
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: }
注意,我不必規(guī)定所聲明的異常變量。盡管它是可選的,但你也可以這樣寫: throw oe; 現(xiàn)在有時還必須留意這個異常。
7.3.2 創(chuàng)建自己的異常類 盡管建議使用預(yù)定義的異常類,但對于實(shí)際場合,創(chuàng)建自己的異常類可能會方便。創(chuàng)建自己的異常類,允許你的異常 類的使用者根據(jù)該異常類采取不同的手段。 在清單 7.9 中出現(xiàn)的異常類 MyImportantException遵循兩個規(guī)則:第一,它用Exception結(jié)束類名。第二,它實(shí)現(xiàn)了 所有三個被推薦的通用結(jié)構(gòu)。你也應(yīng)該遵守這些規(guī)則。 清單 7.9 實(shí)現(xiàn)自己的異常類 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 異常類不能實(shí)現(xiàn)任何特殊的功能,但它完全基于System.Exception類。程序 的剩余部分測試新的異常類,給System.Exception 類使用一個catch 語句。 如果沒有特殊的實(shí)現(xiàn)而只是給MyImportantException定義了三個構(gòu)造函數(shù),創(chuàng)建它又有什么意義呢?它是一個重要的 類型——你可以在catch語句中使用它,代替更為普通的異常類。可能引發(fā)你的新異常的客戶代碼可以按規(guī)定的catch代碼 發(fā)揮作用。 當(dāng)使用自己的名字空間編寫一個類庫時,也要把異常放到該名字空間。盡管它并沒有出現(xiàn)在這個例子中,你還是應(yīng)該 使用適當(dāng)?shù)膶傩裕瑸閿U(kuò)展了的錯誤信息擴(kuò)充你的異常類。
7.4 異常處理的“要”和“不要” 作為最后的忠告之語,這里是對異常引發(fā)和處理所要做和不要做的清單: 。當(dāng)引發(fā)異常時,要提供有意義的文本。 。要引發(fā)異常僅當(dāng)條件是真正異常;也就是當(dāng)一個正常的返回值不滿足時。 。如果你的方法或?qū)傩员粋鬟f一個壞參數(shù),要引發(fā)一個ArgumentException異常。 。當(dāng)調(diào)用操作不適合對象的當(dāng)前狀態(tài)時,要引發(fā)一個 InvalidOperationException異常。 。要引發(fā)最適合的異常。 。要使用鏈接異常,它們允許你跟蹤異常樹。 。不要為正常或預(yù)期的錯誤使用異常。 。不要為流程的正常控制使用異常。 。不要在方法中引發(fā) NullReferenceException或IndexOutOfRangeException異常。
7.5 小結(jié) 這一章由介紹溢出校驗開始。你可以使用編譯器開關(guān)(默認(rèn)是關(guān)),使整個應(yīng)用程序允許或禁止溢出校驗。如果需要 微調(diào)控制,你可以使用校驗和非校驗語句,它允許你使用或不使用溢出校驗來執(zhí)行一段代碼,盡管沒有給應(yīng)用程序設(shè)置開 關(guān)。 當(dāng)發(fā)生溢出時,一個異常就被引發(fā)了。如何處理異常取決于你。我提出了各種途徑,包括你最有可能貫穿整個應(yīng)用程 序使用的:try、catch 和finally 語句。在伴隨的多個例子中,你學(xué)到了它與WIN32結(jié)構(gòu)異常處理(SEH)的差別。 異常處理是給類的用戶; 然而,如果你負(fù)責(zé)創(chuàng)建新的類,就可以引發(fā)異常。有多種選擇:引發(fā)早已捕獲的異常,引發(fā) 存在的框架異常,或者按規(guī)定的實(shí)際目標(biāo)創(chuàng)建新的異常類。 最后,你需要閱讀引發(fā)和處理異常的各種“要”和“不要”。
|