異常處理的作用
無論編碼技術有多好,程序都必須處理任何可能出現的錯誤。如果應用程序不對異常進行任何處理,直接拋出最終會導致應用程序退出。爲了提高程序的健壯性,必須對任何異常進行必要的處理。
異常執行流程
異常處理的關鍵:
1、 定義什麼錯誤
2、 判斷錯誤
3、 從錯誤中恢復
如果在try塊中拋出一個異常,CLR將搜索捕捉類型與異常類型相同的catch塊,如果沒有找到,CLR會調用棧的更高一層搜索與異常類型匹配的捕捉類型,如果到了棧的頂部還未找到,就會發生一個未處理異常。一旦找到一個匹配的catch塊,所有內層的finally塊會被執行;如果沒有找到,內層的finally塊是不會執行的,這一點要特別注意。接着執行匹配catch塊內容,最後是該catch塊對應的finally塊(如果有的話)。
下面代碼展現了異常處理的關鍵流程。
private void SomeMethod() { try { //可能會拋異常的代碼放在這裏 } catch (InvalidOperationException) { //從InvalidOperationException恢復的代碼放在這裏 } catch (IOException) { //從IOException恢復的代碼放在這裏 } catch { //從除上面的異常外的其他異常恢復的代碼放在這裏 //… //捕捉到任何異常,通常要重新拋出異常。 throw; } finally { //這裏的代碼總是執行,對始於try塊的任何操作進行清理 //這裏的代碼總是執行 } // 如果try塊沒有拋出異常,或異常被捕獲後沒有拋出或沒有重新拋出異
//常,就執行這裏的代碼。 }
下面代碼展現三種拋出異常方式對性能的影響
internal class Program { private static void Main(string[] args) { int times = 3000; string test = "test"; int a = 0; Stopwatch stopwatch; //TryParse模式 stopwatch = Stopwatch.StartNew(); for (int i = 0; i < times; i++) { Int32.TryParse(test, out a); } stopwatch.Stop(); Console.WriteLine("TryParse模式避免異常:" + stopwatch.ElapsedMilliseconds); //拋出指定的異常實例 stopwatch.Reset(); stopwatch.Start(); for (int i = 0; i < times; i++) { try { if (!int.TryParse(test, out a)) { throw new ArgumentException(test); } } catch { } } stopwatch.Stop(); Console.WriteLine("拋出指定的異常:" + stopwatch.ElapsedMilliseconds); //在嵌套層裏面不做任何異常處理直接往頂層拋出 stopwatch.Reset(); stopwatch.Start(); for (int i = 0; i < times; i++) { try { MyClass1.Method1(); } catch { } } stopwatch.Stop(); Console.WriteLine("在嵌套層裏面不做任何異常處理直接往頂層拋出:" + stopwatch.ElapsedMilliseconds); // 嵌套層已處理異常,得體恢復返回 stopwatch.Reset(); stopwatch.Start(); for (int i = 0; i < times; i++) { MyClass4.Method4(); } stopwatch.Stop(); Console.WriteLine("嵌套層已處理異常,得體恢復返回:" + stopwatch.ElapsedMilliseconds); Console.ReadLine(); } } internal class MyClass1 { public static void Method1() { MyClass2.Method2(); } } internal class MyClass2 { public static void Method2() { MyClass3.Method3(); } } internal class MyClass3 { public static void Method3() { int.Parse("test"); } } internal class MyClass4 { public static void Method4() { MyClass5.Method5(); } } internal class MyClass5 { public static void Method5() { MyClass6.Method6(); } } internal class MyClass6 { public static string Method6() { string a = ""; try { int.Parse("test"); } catch { a = ""; } return a; } }
運行結果:
從運行結果來看指定拋出異常性能比直接拋異常好一點,爲什麼會這樣?請拍磚。在嵌套層裏面進行異常處理與直接往頂層拋異常的性能相差不大,
但最重要的地方是對錯誤進行恢復。異常處理的重點是錯誤的恢復、處理。
微軟對異常處理方式建議
對於可能在常見方案中引發異常的成員,可以考慮使用 Tester-Doer 模式來避免與異常相關的性能問題。
例如,對於函數的參數問題,就必須創建檢測類檢測輸入是否正確。對於可能在常見方案中引發異常的成員,可以考慮使用 TryParse 模式來避免與異常相關的性能問題。
TryParse模式參考int.TryParse等.就是創建正常運行的條件避免拋出異常.
對於可能在常見方案中引發異常的成員,可以考慮使用 Tester-Doer 模式來避免與異常相關的性能問題。
Tester-doer 模式將分爲了調用,可能會引發異常,分成兩個部分: 一種測試儀和實幹家。 Tester 對可能導致 Doer 引發異常的狀態執行測試。 測試恰好插入在引發異常的代碼之前,從而防範異常發生。
下面的代碼示例演示此模式的 Doer 部分。 該示例包含一個方法,在向該方法傳遞 null(在 Visual Basic 中爲Nothing)值時該方法將引發異常。 如果頻繁地調用該方法,就可能會對性能造成不良影響。
public class Doer { // Method that can potential throw exceptions often. public static void ProcessMessage(string message) { if (message == null) { throw new ArgumentNullException("message"); } } // Other methods... }
下面的代碼示例演示此模式的 Tester 部分。 該方法利用一個測試來避免在 Doer 將引發異常時調用 Doer (ProcessMessage)。
public class Tester { public static void TesterDoer(ICollection<string> messages) { foreach (string message in messages) { // Test to ensure that the call // won't cause the exception. if (message != null) { Doer.ProcessMessage(message); } } } }
對於可能在常見方案中引發異常的成員,可以考慮使用 TryParse 模式來避免與異常相關的性能問題。若要實現 TryParse 模式,
需要爲執行可在常見方案中引發異常的操作提供兩種不同的方法。 第一種方法 X, 執行該操作並在適當時引發異常。
第二種方法 TryX, 不引發異常,而是返回一個 Boolean 值以指示成功還是失敗。
由對 TryX 的成功調用所返回的任何數據都通過使用 out(在 Visual Basic 中爲 ByRef)參數予以返回。Parse 和 TryParse 方法就是此模式的示例。
爲每個使用 TryParse 模式的成員提供一個引發異常的成員。只提供 TryX 方法幾乎在任何時候都不是正確的設計,因爲使用該方法需要了解 out 參數。
此外,對於大多數常見方案來說,
異常對性能的影響不會構成問題;因此應在大多數常見方案中提供易於使用的方法。
結論: 由微軟提供的Doer-Tester模式可知處理異常的方式最好是不要引發異常,在可能拋出異常的地方進行必要的檢查和驗證。例如對實參進行驗證。
總結:實現自己的方法時,如果方法無法完成方法名所指明的任務,就應該拋出異常或對錯誤進行得體恢復。拋出異常時應該詳細說明方法爲什麼無法完成任務。對於未指定處理的異常通常被寫入日誌,開發人員必須修復該異常所在的bug。