.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; }
}
複製程式碼
常見的異常有 IndexOutOfRangeException
、NullReferenceException
、ArgumentNullException
等等。
每一種異常,都對應於一個特定的情形。在實際專案中,如果需要自定義異常,也應該遵循這個原則。比如 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
將對映為 UnauthorizedAccessException
、E_OUTOFMEMORY
對映為 OutOfMemoryException
,等等
如果 HRESULT
為自定義值,或 CLR
無法將其對映成預定義的具體託管異常。執行時會引發 COMException
異常, 其 ErrorCode
屬性包含具體的 HRESULT
值
編碼建議
設計良好的異常處理機制可以防止應用崩潰。這部分介紹了在實際專案中處理和建立異常的一些建議
- 合理使用
try...catch
,過於頻繁的使用,會造成效能低下(如果某些異常一直出現)。況且,我們也不應該過於依賴異常處理機制,對業務邏輯中具體情況進行良好的處理,比用try...catch
更有意義。比如,當我們嘗試關閉已關閉的連線時,就會引發InvalidOperationException
異常。為了避免這個異常,我們可以在嘗試關閉前,通過使用if
語句檢查連線狀態,避免該情況 - 對於某些情況,如果在返回
null
,或者型別的預設值的情況下,不會影響對方法的理解。那麼我們應該返回型別的預設值或null
,而不是去引發一個異常 - 如果可以不引發異常,那麼就不引發異常。這時我們只需要在程式中對特定的情況進行處理修正即可。一般情況下,如果引發異常無法帶來好處,或者並沒有讓我們提供的介面、方法等更易於理解,那就沒必要引發異常
- 僅在異常需要攜帶某些自定義資料的情況下,去自定義異常(該異常類應該以
Exception
結尾)。否則,我們使用系統預定義異常即可 - 在每個異常中都包含一個本地化描述字串。一般情況下,如果不是跨國籍合作,我們可以都使用中文,或者一律使用英文,也可以混搭,這個根據公司專案的情況而定
在實際專案中,我們應該參考上面的建議,以幫助我們寫出效能和可維護性都較好的程式碼
至此,這篇文章的內容講解完畢。 歡迎關注公眾號【嘿嘿的學習日記】,所有的文章,都會在公眾號首發,Thank you~