1.為什麼不要給每個方法都寫try catch
為每個方法都編寫try catch是錯誤的做法,理由如下:
a.重複巢狀的try catch是無用的,多餘的。
這一點非常容易理解,下面的示例程式碼中,OutsideMethodA中的try catch是不起作用的。
class NestedTryCatch { internal void OutsideMethodA() { try { this.InsideMethodB(); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } } private void InsideMethodB() { try { this.ExceptionMethod(); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } } private void ExceptionMethod() { throw new NotImplementedException("You did't implement this method!"); } }
b.多餘的try catch會掩蓋嚴重的bug,將bug珍藏在log裡並不會增值。
下面的程式碼中,一旦引數uri為null,意味著程式邏輯必然有bug,存在錯誤的呼叫。與其將這個bug和HttpRequestException混在一起寫log,然後相忘於江湖。不如大大方方在開發階段就每次crash,強迫必須修復隱藏的邏輯錯誤。
同時我們可以看到,catch裡再次返回了null,這又是一種不負責任給上層程式碼挖坑的行為。上層程式碼兩眼一黑,就得一個null,啥也不知道,估計也不敢問。
註釋的部分給出了兩種解決方案,Assert或者主動throw。
internal async Task<string> DownloadContent(string uri) { //Debug.Assert(!string.IsNullOrEmpty(uri)); //if (string.IsNullOrEmpty(uri)) //{ // throw new ArgumentNullException("uri is null"); //} try { using (var httpClient = new HttpClient()) { return await httpClient.GetStringAsync(uri); } } catch (Exception ex) { Console.WriteLine(ex.ToString()); return null; } }
c.當程式因Exception進入不可繼續的狀態時,通過try catch避免程式crash,除了可以稍微體面地退出,並沒有更大意義。
例如網路遊戲在執行過程中,發生了錯誤。本地資料與伺服器不再同步,是不會允許繼續執行,也不會承認期間產生的本地資料。
硬用程式碼舉例的話,就比如在建構函式裡搞個try catch吞掉Exception,這個new出來的例項誰還敢用的,請站出來……
d.都知道空的try catch是錯誤的。
try { …… } catch{ }
難道加個日誌就會產生質變了嘛?
try { …… } catch (Exception ex) { Log.Error(“xxxx方法失敗了!”); }
2.何時使用try catch?只提出問題不給出解決方案,會被罵耍流氓。下面我們來分析幾個適於新增try catch的場景。
a.僅在你真的打算,並且知道如何處理當前的Exception時,加try catch。比較明顯的場景是網路請求中的retry。
public async Task<string> HandleHttpRequestExceptionAsync() { HttpClient client = new HttpClient(); try { return await client.GetStringAsync("http://www.ajshdgasjhdgajdhgasjhdgasjdhgasjdhgas.tk/"); } catch (HttpRequestException ex) { //Simulate to try again. //log here then retry return await client.GetStringAsync("http://www.dell.com/"); } finally { client?.Dispose(); } }
b.當常規流程控制無法避免異常時,加try catch。
通常可以用if來避免的問題,就不應通過try catch處理。反例如IO處理,無法確認使用者會不會拔U盤,該情況下需catch IOException。
c.功能性的類庫中的API缺乏業務邏輯,不知道如何處理Exception時,不應加try catch。應將錯誤拋給上層,由存在業務邏輯的呼叫方處理。
比較典型的,在使用Microsoft UI Automation的API時,找元素的API可能會丟擲COMException。API本身認為呼叫方傳參錯誤,傳入了不存在元素的ID。但上層的呼叫程式碼會知道,是因為當前頁面未載入完全。如果我們希望在這裡retry或者忽略這個錯誤,try catch是合理的。
d.為了體面的退出。
在頂層加入try catch記錄log是可行的。呼叫堆疊的資訊會完整的儲存下來。(針對Task的異常堆疊丟失問題,請看《.NET Core學習筆記(3)——async/await中的Exception處理》)
3.在頂層程式碼應用try catch的一些可行做法
a.如果我們真的害怕且不能接受crash。
可以試著在Main方法里加個try catch,然後記錄log。
b.不是主執行緒的UnHandle Exception。
通過AppDomain.UnhandledException來處理。
public static void Main() { AppDomain currentDomain = AppDomain.CurrentDomain; currentDomain.UnhandledException += new UnhandledExceptionEventHandler(MyHandler); try { throw new Exception("1"); } catch (Exception e) { Console.WriteLine("Catch clause caught : {0} \n", e.Message); } throw new Exception("2"); } static void MyHandler(object sender, UnhandledExceptionEventArgs args) { Exception e = (Exception)args.ExceptionObject; Console.WriteLine("MyHandler caught : " + e.Message); Console.WriteLine("Runtime terminating: {0}", args.IsTerminating); }
預設情況下.NET 程式將會退出,因為此時的程式因為這個unhandle exception,被認為進入了未知,且不可繼續的狀態。
此時即使通過某些特殊手段保持程式不退出,也沒有任何意義。unhandle exception的意思就是有crash bug沒處理。開發階段幹嘛去了。
https://docs.microsoft.com/en-us/dotnet/standard/threading/exceptions-in-managed-threads#application-compatibility-flag
上述連結提供了程式不退出的可能選項,但我認為實不可取。
4.判斷Exception型別的一些技巧,
仍然以HttpClient.GetStringAsync舉例,我們可以通過檢視MSDN得知該方法可能丟擲如下幾個Exceptions:
a.AugumentNullException
我們上文提過了,在上層呼叫程式碼可以通過null check來避免,或者主動丟擲exception。
b.HttpRequestException
網路錯誤都會拋這個異常,通常我們需要捕獲該異常,並通過異常中返回的Status或是其他資訊來針對性處理。
c.TaskCanceledException
在以下兩種情況會被丟擲:
-
- 指定了HttpClient.Timeout同時本次網路請求超出指定時間
- 在使用Task非同步程式設計時,在Task Completed之前呼叫CancellationTokenSource物件的Cancel()方法
那麼在寫程式碼的時候,就要判斷是否是.NET Core,同時符合以上兩點。否則就無需對該異常新增處理。
舉著例子更重要的目的是想說,除了頂層程式碼的最後一道用於記錄log的try exception。沒有任何理由用到基類Exception。
文中提到的示例程式碼可以在這裡找到:
https://github.com/manupstairs/PracticeOfException
本篇提到了處理Exception時的一些實踐經驗,且為一家之言,如有錯誤的地方還請指出。