前言
本文已同步到http://www.cnblogs.com/aehyok/p/3624579.html。本文主要來學習以下幾點建議
建議61、避免在finally內撰寫無效代碼
建議62、避免嵌套異常
建議63、避免“吃掉”異常
建議64、為循環增加Tester-Doer模式而不是將try-catch置於循環內
建議61、避免在finally內撰寫無效代碼
先直接來看一下三個簡單的try catch方法
public class User { public string Name { get; set; } } class Program { static void Main(string[] args) { Console.WriteLine(Test1()); Console.WriteLine(Test2()); Console.WriteLine(Test3().Name); Console.ReadLine(); } public static int Test1() { int i = 0; try { i = 1; } finally { i = 2; Console.WriteLine("\t 將int結果改為2,finnally執行完畢。"); } return i; } public static int Test2() { int i = 0; try { return i = 1; } finally { i = 2; Console.WriteLine("\t 將int結果改為2,finnally執行完畢。"); } }
看完代碼你心里大概也有了一個答案了吧
這些如果通過IL來解釋,還是比較容易的,在此就不進行贅述了。
在CLR中,方法的參數以及返回值都是用棧來保存的。在方法內部,會首先將參數依次壓棧,當需要使用這些參數的時候,方法會直接去棧里取用參數值,方法返回時,會將返回值壓入棧頂。如果參數的類型是值類型,壓棧的就是復制的值,如果是引用類型,則在方法內對於參數的修改也會帶到方法外。
建議62、避免嵌套異常
在建議59中已經強調過,應該允許異常在調用堆棧中往上傳播,不要過多使用catch,然后再throw。果斷使用catch會帶來兩個問題:
1、代碼更多了。這看上去好像你根本不知道該怎么處理異常,所以你總在不停地catch.
2、隱藏了堆棧信息,使你不知道真正發生異常的地方。
來看一下下面的代碼
static void Main(string[] args) { try { Console.WriteLine("NoTry\n"); MethodNoTry(); } catch (Exception exception) { Console.WriteLine(exception.StackTrace); } try { Console.WriteLine("WithTry\n"); MethodWithTry(); } catch (Exception exception) { Console.WriteLine(exception.StackTrace); } Console.ReadLine(); } public static int Method() { int i = 0; return 10 / i; } public static void MethodNoTry() { Method(); } public static void MethodWithTry() { try { Method(); } catch (Exception exception) { throw exception; } }
執行結果
可以發現,MethodNoTry的方法可以查看到發生異常錯誤的地方,而MethodWithTry根本不清楚發生錯誤的地方了。調用的堆棧倍重置了。如果這個方法還存在另外的異常,在UI層將永遠不知道真正發生錯誤的地方,給開發者帶來不小的麻煩。
除了在建議59中提到的需要包裝異常的情況外,無故地嵌套異常是我們要極力避免的。當然,如果真得需要捕獲這個異常來恢復一些狀態,然后重新拋出,代碼來起來應該可以這樣:
try { MethodWithTry(); } catch(Exception) { ///工作代碼 throw; }
或者稍作改動
try { MethodWithTry(); } catch { ///工作代碼 throw; }
盡量避免下面這樣引發異常:
try { MethodWithTry(); } catch(Exception exception) { ///工作代碼 throw exception; }
直接throw exception而不是throw將會重置堆棧消息。
建議63、避免“吃掉”異常
看了建議62之后,你可能已經明白,嵌套異常是很危險的行為,一不小心就會將異常堆棧信息,也就是真正的Bug出處隱藏起來。但這還不是最嚴重的行為,最嚴重的就是“吃掉”異常,即捕獲然后不向上層throw拋出。
避免“吃掉”異常,並不是說不應該“吃掉”異常,而是這里面有個重要原則:該異常可悲預見,並且通常情況它不能算是一個Bug。
想象你正在對上萬份文件進行解密,這些文件來自不同的客戶端,很有可能存在文件倍破壞的現象,你的目標就是要講解密出來的數據插入數據庫。這個時候,你不得不忽略那些解密失敗的問題,讓這個過程進行下去。當然,記錄日志是必要的, 因為后期你可能會倍解密失敗的文件做統一的處理。
另外一種情況,可能連記錄日志都不需要。在對上千個受控端進行控制的分布式系統中,控制端需要發送心跳數據來判斷受控端的在線情況。通常的做法是維護一個信號量,如果在一個可接受的阻滯時間如(如500ms)心跳數據發送失敗,那么控制端線程將不會收到信號,即可以判斷受控端的斷線狀態。在這種情況下,對每次SocketException進行記錄,通常也是沒有意義的。
本建議的全部要素是:如果你不知道如何處理某個異常,那么千萬不要“吃掉”異常,如果你一不小“吃掉”了一個本該網上傳遞的異常,那么,這里可能誕生一個BUg,而且,解決它會很費周折。
建議64、為循環增加Tester-Doer模式而不是將try-catch置於循環內
如果需要在循環中引發異常,你需要特別注意,因為拋出異常是一個相當影響性能的過程。應該盡量在循環當中對異常發生的一些條件進行判斷,然后根據條件進行處理。可以做一個測試:
static void Main(string[] args) { CodeTimer.Initialize(); CodeTimer.Time("try..catch..", 1, P1); CodeTimer.Time("Tester-Doer", 1, P2); Console.ReadLine(); } public static void P1() { int x = 0; for (int i = 0; i < 1000; i++) { try { int j = i / x; } catch { } } }
差距相當明顯。以上代碼中,我們預見了代碼可能會發生DivideByZeroException異常,於是,調整策略,對異常發生的條件進行了特殊處理:Continue,讓效率得到了極大的提升。