溫故之.NET異常處理

JameLee發表於2018-07-03

.NET 提供了一種統一的方式來報告應用程式的錯誤,即通過引發異常來指示具體問題。這相比於 Win32 時代的錯誤處理(通過 GetLastError 或者 HRESULT 的方式 ),不但要簡單明瞭得多,還更容易維護。通過監控程式可能引發的異常,並對異常做出相應的處理(比如資料恢復、日誌記錄),可以提高程式的可靠性、可維護性

對異常的理解

異常,即程式在執行過程中,遇到的錯誤或意外的行為

.NET 中,異常都是從 System.Exception 類繼承。具體的異常由發生問題的程式碼引發,然後它在堆疊中向上傳遞,直到應用程式對其進行處理或者程式終止為止

Exception 定義如下

public class Exception : ISerializable, _Exception {
    public Exception();
    public Exception(string message);
    public virtual string Source { get; set; }
    public virtual string HelpLink { get; set; }
    // 包含可用於確定錯誤位置的堆疊跟蹤
    // 如果有可用的除錯資訊,則堆疊跟蹤包含原始檔名和程式行號
    public virtual string StackTrace { get; }
    public MethodBase TargetSite { get; }
    public Exception InnerException { get; }
    // 一般情況,我們可以通過這個屬性,來理解具體的異常
    public virtual string Message { get; }
    public int HResult { get; protected set; }
    public virtual IDictionary Data { get; }
}
複製程式碼

常見的異常有 IndexOutOfRangeExceptionNullReferenceExceptionArgumentNullException 等等。

每一種異常,都對應於一個特定的情形。在實際專案中,如果需要自定義異常,也應該遵循這個原則。比如 IndexOutOfRangeException 針對的就是陣列或集合訪問越界的情形

使用 try...catch 塊捕獲異常

對於有可能產生異常的程式碼,我們可以使用 try...catch 將這些程式碼包圍起來,並在 catch 子句中指明需要捕獲的異常,同時可以在 catch 的程式碼塊內,對該異常做出相應的處理

一般情況下,我們應該在 catch 子句中指明具體的異常,因為這樣我們就能很方便地對不同的異常做出具體的處理,以便程式儘可能的從異常中恢復過來。比如像下面這樣

try {
    using (StreamReader sr = File.OpenText("data.txt")) {
        Console.WriteLine($"First Line: {sr.ReadLine()}");
    }
} catch (UnauthorizedAccessException e) {
    // 針對未授權做出處理。比如請求管理員許可權
} catch (DirectoryNotFoundException e) {
    // 針對目錄不存在處理。比如彈窗提示使用者
} catch (FileNotFoundException e) {
    // 針對檔案不存在處理。比如彈窗處理
}
複製程式碼

當然,如果我們僅僅是為了捕獲異常,並記錄日誌。此時我們只需要在 catch 子句中,使用 Exception 即可(Exception類為所有異常類的基類),如下

try {
    using (StreamReader sr = File.OpenText("data.txt")) {
        Console.WriteLine($"First Line: {sr.ReadLine()}");
    }
} catch (Exception e) {
    // 這種情況下,我們可以通過日誌記錄的模組,來記錄此次異常
    // 以便後期維護使用
}
複製程式碼

如果我們在處理了特定的異常之後,希望將其他意料之外的異常也記錄在日誌中

需要 特別注意的是:Exception 只能放置在最後的 catch 子句中,因為它是其他異常的基類

示例如下

try {
    using (StreamReader sr = File.OpenText("data.txt")) {
        Console.WriteLine($"First Line: {sr.ReadLine()}");
    }
} catch (UnauthorizedAccessException e) {
    // 針對未授權做出處理。比如請求管理員許可權
} catch (DirectoryNotFoundException e) {
    // 針對目錄不存在處理。比如彈窗提示使用者
} catch (FileNotFoundException e) {
    // 針對檔案不存在處理。比如彈窗處理
} catch(Exception e) {
    // 通過日誌記錄的模組,記錄意料之外的異常
}
複製程式碼

因此,有以下結論:
catch 子句中,子類應該放在其父類的前面。否則,將無法捕獲具體子類的異常。比如前面的這段程式碼,如果我們將 Exception e 放置在其他的異常之前,那麼其他異常的具體處理邏輯都無法執行

我們應該如何引發異常

我們可以使用 throw 語句顯式引發異常。使用方式有兩種

方式一

throw 異常物件的方式。比如,我們可以通過 throw new ArgumentNullException() 來引發一個引數為空的異常;也可以使用 throw new Exception() 來引發,不過 不建議這樣使用。使用特定情形的異常物件是最好的做法

使用方式如下

public void ProcessData(int[] source, int from, int count) {
    if (source == null)
        throw new ArgumentNullException("source", "The source you provided cannot be null");
    if (from < 0 || from >= source.Length)
        throw new IndexOutOfRangeException("The 'from' parameter is out of range");
    // ...
    // 其他邏輯
    // ...
}
複製程式碼

這種引發異常的方式,在我們的寫公共的類庫的時候會經常用到。因為在公共類庫中,我們需要將發生問題的詳細資訊傳遞出去,以方便使用類庫的開發者除錯

方式二

直接使用 throw; 的方式,這種使用方式只能存在於 catch 子句中。當我們在處理了具體的異常之後,仍然希望上層能夠捕獲此異常的時候非常有用

使用方式如下

try {
    // 業務邏輯程式碼
} catch (UnauthorizedAccessException e) {
    // 異常處理邏輯

    // 將異常傳遞出去
    throw;
}
複製程式碼

建立自定義異常

在預定義的異常不符合業務需求的情況下(比如預定義異常無法攜帶我們需要的資訊時),我們可以通過從 Exception 類派生來建立自己的異常類

比如,我們需要一個資料庫中使用者不存在的異常,則可以按如下方式處理

public class UserNotExistException : Exception {
    public string UserName { get; }
    public string UserId { get; }

    public UserNotExistException(string userName, string userId) {
        this.UserName = userName;
        this.UserId = userId;
    }
}
複製程式碼

在使用者不存在的情況下,通過引發此 UserNotExistException 異常,我們可以很容易的獲取到不存在的使用者的ID及暱稱

使用 finally 塊

定義在 finally 塊中的程式碼,其表示:無論 try 塊中是否有異常發生,都會執行。常見於資源的清理,比如檔案操作、網路操作或資料庫操作完成之後

如下程式碼所示

StreamReader sr = null;
try {
    sr = File.OpenText("data.txt");
    Console.WriteLine($"First Line: {sr.ReadLine()}");
} catch (UnauthorizedAccessException e) {

} catch (DirectoryNotFoundException e) {

} catch (FileNotFoundException e) {

} catch (Exception e) {

} finally {
    // 無論前面是否發生異常,我們都需要銷燬檔案資源 
    if (sr != null) {
        sr.Dispose();
    }
}
複製程式碼

COM 互操作異常

一般情況下, 如果因COM方法失敗而返回 HRESULT,執行時會將其對映為可由託管程式碼捕獲的異常。例如,E_ACCESSDENIED 將對映為 UnauthorizedAccessExceptionE_OUTOFMEMORY 對映為 OutOfMemoryException,等等

如果 HRESULT 為自定義值,或 CLR 無法將其對映成預定義的具體託管異常。執行時會引發 COMException 異常, 其 ErrorCode 屬性包含具體的 HRESULT

編碼建議

設計良好的異常處理機制可以防止應用崩潰。這部分介紹了在實際專案中處理和建立異常的一些建議

  • 合理使用 try...catch,過於頻繁的使用,會造成效能低下(如果某些異常一直出現)。況且,我們也不應該過於依賴異常處理機制,對業務邏輯中具體情況進行良好的處理,比用 try...catch 更有意義。比如,當我們嘗試關閉已關閉的連線時,就會引發 InvalidOperationException 異常。為了避免這個異常,我們可以在嘗試關閉前,通過使用 if 語句檢查連線狀態,避免該情況
  • 對於某些情況,如果在返回 null,或者型別的預設值的情況下,不會影響對方法的理解。那麼我們應該返回型別的預設值或null,而不是去引發一個異常
  • 如果可以不引發異常,那麼就不引發異常。這時我們只需要在程式中對特定的情況進行處理修正即可。一般情況下,如果引發異常無法帶來好處,或者並沒有讓我們提供的介面、方法等更易於理解,那就沒必要引發異常
  • 僅在異常需要攜帶某些自定義資料的情況下,去自定義異常(該異常類應該以 Exception 結尾)。否則,我們使用系統預定義異常即可
  • 在每個異常中都包含一個本地化描述字串。一般情況下,如果不是跨國籍合作,我們可以都使用中文,或者一律使用英文,也可以混搭,這個根據公司專案的情況而定

在實際專案中,我們應該參考上面的建議,以幫助我們寫出效能和可維護性都較好的程式碼


至此,這篇文章的內容講解完畢。 歡迎關注公眾號【嘿嘿的學習日記】,所有的文章,都會在公眾號首發,Thank you~

公眾號二維碼