.NET Core學習筆記(7)——Exception最佳實踐

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 catch。沒有任何理由用到基類Exception。

文中提到的示例代碼可以在這裏找到:
https://github.com/manupstairs/PracticeOfException
本篇提到了處理Exception時的一些實踐經驗,且爲一家之言,如有錯誤的地方還請指出。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章