.Net 異常最佳做法


異常信息原因

異常是易於濫用的那些構造之一。這可能包括不應該在應有的情況下引發異常或在沒有充分理由的情況下捕獲異常。還有一個引發錯誤異常的問題,它不僅無助於我們,而且會使我們困惑。另一方面,存在正確處理異常的問題。如果使用不當,異常處理會變得更糟。所以,在本文中,我將簡單介紹一些有關引發和處理異常的最佳實踐。展示如何拋出適當的異常可以為我們節省很多調試方面的麻煩。我還將討論當我們想要查找錯誤時不良的異常處理如何引起誤導。

拋出異常

何時拋出異常

在很多情況下,拋出異常是有意義的,在這里,我將對其進行描述並討論為什么拋出它們是一個好主意。請注意,本文中的許多示例都經過簡化以證明這一點。例如,沒有使用一種方法來檢索數組元素。或者在某些情況下,我沒有使用以前提倡的技術來關注當前觀點。因此,自然而然地,示例並不試圖在所有方面都成為異常處理的理想形式,因為這樣會引入額外的元素,從而可能使讀者分心。

1:不可能完成過程並給出結果(快速失敗)

static void FindWinner(int[] winners)
{
    if (winners == null)
    {
        throw new System.ArgumentNullException($"參數 {nameof(winners)} 不能為空", nameof(winners));
    }
    
    OtherMethodThatUsesTheArray(winner);
}

假設我們有上述方法,在這里我們拋出一個異常,因為從這種方法中獲得沒有贏家數組的結果是不可能的。另一個要點是該方法的用戶易於使用。想象一下,我們沒有引發異常,而是將數組傳遞給OtherMethodThatUsesTheArray method,而該方法引發了NullReferenceException。通過不拋出異常,調試變得更加困難。因為此代碼的調試器必須首先查看OtherMethodThatUsesTheArray方法,因為這就是錯誤的來源。然后,他找出贏家的論點是產生此錯誤的地方。當我們拋出異常時,我們確切地知道錯誤的根源,而不必在代碼庫中追逐錯誤。另一個問題是不必要的資源使用,假設在達到框架發生異常之前,我們進行了一些昂貴的處理。現在,如果該方法在沒有相關參數或您所沒有的情況下無法提供我們的結果,則實際上浪費了很多資源,而實際上該方法最初可能無法成功。請記住,當可能發生錯誤時,我們不會拋出異常,但是當錯誤阻止流程時,我們會拋出異常。有時我們也可以避免使用異常,而使用try-stuff模式int.TryParse並且不拋出異常。

2:給定對象的當前狀態,調用對象的成員可能會產生無效的結果,或者可能會完全失敗

void WriteLog(FileStream logFile)
{
    if (!logFile.CanWrite)
    {
        throw new System.InvalidOperationException("日志文件不能是只讀的");
    }
    // Else write data to the log and return.
}

在這里,傳遞給WriteLog方法的文件流以不可寫的方式進行創建。在這種情況下,我們知道此方法將不起作用。因為我們無法登錄到不可寫的文件。另一個例子是當我們有一個類,我們希望在收到它時處於特定狀態。通過拋出異常並節省資源和調試時間,我們又一次快速失敗。

捕獲通用的非特定異常並引發更特定的異常


void WriteLog(FileStream logFile)
{
    if (!logFile.CanWrite)
    {
        throw new System.InvalidOperationException("日志文件不能是只讀的");
    }
    // Else write data to the log and return.
}

關於異常,有一個經驗法則,程序產生的異常越具體,調試和維護就越容易。換句話說,通過這樣做,我們的程序會產生更准確的錯誤。因此,我們應始終努力盡可能地拋出更具體的異常。這就是為什么拋出異常喜歡System.Exception,System.SystemException,System.NullReferenceException,或者System.IndexOutOfRangeException是不是一個好主意。最好不要使用它們,並且將它們看作是錯誤消息是由框架生成的信號,我們將花費大量時間進行調試。在上面的代碼中,您看到我們抓住IndexOutOfRangeException並拋出了一個新的ArgumentOutOfRangeException向我們顯示了實際錯誤的來源。我們還可以使用它來捕獲框架生成的異常並引發新的自定義異常。這使我們可以添加其他信息,或者可能以不同的方式進行處理。只要確保將原始異常作為內部異常傳遞到自定義異常中,否則stacktrace將丟失。

4:例外情況引發異常

這聽起來似乎很明顯,但有時可能會很棘手。我們的程序中有某些事情會發生,我們不能將它們視為錯誤。因此,我們不會拋出異常。例如,搜索查詢可能返回空,或者用戶登錄嘗試可能失敗。在這種情況下,最好返回某種有意義的消息,然后拋出異常。正如史蒂夫·麥康奈爾(Steve McConnell)在《代碼完整》一書中所說的那樣,“例外應該保留給真正的例外 ”,而不是期望的例外。

不要使用異常來更改程序的流程

以下面的代碼為例。

[HttpPost]
public ViewResult CreateProduct(CreateProductViewModel viewModel)
{
    try
    {
        ValidateProductViewModel(viewModel);
        CreateProduct(viewModel);
    }
    catch (ValidationException ex)
    {
        return View(viewModel);
    }
}

我在一些需要處理的舊代碼中看到了這種模式。如您所見,ValidateProductViewModel 如果視圖模型無效,則由於某種原因該方法將引發異常。然后,如果視圖模型無效,它將捕獲該模型並返回錯誤的視圖。我們最好將上面的代碼更改為下面的代碼

[HttpPost]
public ViewResult CreateProduct(CreateProduct viewModel)
{
    bool viewModelIsValid = ValidateProductViewModel(viewModel);
        
    if(!viewModelIsValid) return View(viewModel); 
        
    CreateProduct(viewModel);
    
     return View(viewModel); 
}

在這里,負責驗證的方法在視圖模型無效的情況下返回布爾值,而不是引發異常

不返回錯誤代碼,而是引發異常

拋出異常總是比返回錯誤代碼更安全。原因是如果調用代碼忘記檢查或返回錯誤代碼並繼續執行該怎么辦?但是,如果我們拋出異常,那將不會發生。

確保清除拋出異常的任何副作用

private static void MakeDeposit(Account account,decimal amount)
{
    try
    {
        account.Deposit(amount);
    }
    catch
    {
        account.RollbackDeposit(amount);
        throw;
    }
}

在這里,我們知道調用deposit方法時可能會發生錯誤。我們應該確保如果發生異常,則對系統的任何更改都會回滾。

try
{
    DBConnection.Save();
}
catch
{
    // 回滾數據庫操作
    DBConnection.Rollback();

    // 重新拋出異常,讓外界知道錯誤消息
    throw;
}

您也可以使用事務作用域,而不用這種方式進行處理。請記住,您也可以在“最終阻止”中執行此操作。

如果您捕獲了一個異常並且無法正確處理它,請確保將其重新拋出

在某些情況下,當我們捕獲到異常但我們不打算處理它時,也許我們只是想記錄它。就像是:

try
{
    conn.Close();
}
catch (InvalidOperationException ex)
{
    _logger.LogError(ex.Message, ex);
    
     //不好的做法,堆棧跟蹤丟失了
    //拋出ex; 
    
    //優良作法,保持堆棧跟蹤
    拋出;
    throw;
}

如您在上面的代碼中看到的那樣,我們不僅應該重新拋出異常,而且還應該以不丟失堆棧跟蹤的方式重新拋出該異常。在這種情況下,如果使用throw ex,則會丟失堆棧跟蹤,但是如果使用前不帶instace ex的throw,則會保留堆棧跟蹤。

不要將異常用作參數或返回值

在大多數情況下,使用Exception作為參數或返回值沒有意義。也許只有當我們在異常工廠中使用它時,它才有意義。

Exception AnalyzeHttpError(int errorCode) {
    if (errorCode < 400) {
         throw new NotAnErrorException();
    }
    switch (errorCode) {
        case 403:
             return new ForbiddenException();
        case 404:
             return new NotFoundException();
        case 500:
             return new InternalServerErrorException();
        …
        default:
             throw new UnknownHttpErrorCodeException(errorCode);
     }
}

防止程序拋出異常(如果可能),導致異常昂貴

try
{
    conn.Close();
}
catch (InvalidOperationException ex)
{
    Console.WriteLine(ex.GetType().FullName);
    Console.WriteLine(ex.Message);
}

以上面的代碼為例,我們將用於關閉連接的代碼放在try塊中。如果連接已經關閉,它將引發異常。但是也許我們可以在不導致程序引發異常的情況下實現相同的目標?

if (conn.State != ConnectionState.Closed)
{
    conn.Close();
}

如您所見,try塊是不必要的,如果連接已經關閉,它將導致程序引發異常,並且異常的開銷比check開銷大。

創建自己的異常類

框架並未涵蓋所有可能發生的異常。有時我們需要創建自己的異常類型。可以將它們定義為類,就像C#中的任何其他類一樣。我們創建自定義異常類通常是因為我們想以不同的方式處理該異常。或那種特殊的異常對我們的應用程序非常關鍵。要創建自定義異常,我們需要創建一個派生自System.Exception的類。

[Serializable()]
public class InvalidDepartmentException : System.Exception
{
    public InvalidDepartmentException() : base() { }
    public InvalidDepartmentException(string message) : base(message) { }
    public InvalidDepartmentException(string message, System.Exception inner) : base(message, inner) { }

    // 當異常從遠程服務器傳播到客戶端時,需要序列化構造函數
    protected InvalidDepartmentException(SerializationInfo info, StreamingContext context) { }
}

在這里,派生類定義了四個構造函數。一種默認構造函數,一種用於設置message屬性,一種用於同時設置Message和InnerException。第四個構造函數用於序列化異常。另請注意,異常應可序列化。

處理(捕獲)異常

何時捕獲異常
捕獲異常比拋出異常更容易被濫用。但是,當應用程序達到維護階段時,這種濫用會導致很多痛苦。在以下部分中,我將描述有關處理異常的一些最佳實踐。

1:當異常處理

我在許多應用程序中看到過,try塊用於抑制異常。但這不是try-catch塊的用途。通過這樣的嘗試,我們改變了系統的行為,使發現錯誤的難度超過了應有的程度。這種現象非常普遍,以至於我們對其濫用有一個術語,即Pokemon異常處理。大多數情況下,發生的事情是try-catch塊吞沒了錯誤,錯誤最終在我們應用程序中的其他位置而不是原始位置出現。真正的痛苦是,大多數時候錯誤消息根本沒有任何意義,因為它不是原始錯誤的來源。這使得調試體驗令人沮喪。

2:在實際可以處理異常並從異常中恢復時使用嘗試阻止

這種情況的一個很好的例子是,當程序提示用戶輸入文件和文件的路徑時,該路徑不存在。如果應用程序拋出錯誤,我們可以從中恢復,方法可能是捕獲異常並要求用戶輸入另一個文件路徑。因此,您應該將捕獲塊的順序從最具體的到最不具體的。

public void OpenFile(string filePath)
{
  try
  {
     File.Open(path);
  }
  catch (FileNotFoundException ex)
  {
     Console.WriteLine("找不到指定的文件路徑,請輸入其他路徑");
     PromptUserForAnotherFilePath();
  }
}

您可以使用的另一件事是異常過濾器。異常過濾器的工作方式類似於catch塊的if語句。如果檢查結果為true,則執行catch塊,否則將忽略catch塊。

private static void Filter()
{
    try
    {
        A();
    }
    catch (OperationCanceledException exception) when (string.Equals(nameof(ExceptionFilter), exception.Message, StringComparison.Ordinal))
    {
    }
}

3:您想捕獲一個通用異常並拋出一個具有更多上下文的更具體的異常

int GetInt(int[] array, int index)
{
    try
    {
        return array[index];
    }
    catch(System.IndexOutOfRangeException e)
    {
        throw new System.ArgumentOutOfRangeException("Parameter index is out of range.", e);
    }
}

以上面的代碼為例。在這里,我們捕獲IndexOutOfBound 異常並拋出ArgumentOutOfRangeException。這樣,我們就可以更清楚地了解錯誤的來源,並且可以更快地找到問題的根源。

4:您想部分處理異常並將其傳遞給進一步處理

try
{
    // Open File
}
catch (FileNotFoundException e)
{
    _logger.LogError(e);
    // Re-throw the error.
    throw;     
}

在上面的示例中,我們捕獲了異常,記錄了所需的信息,然后重新拋出了異常。

僅在應用程序的最高層出現捕獲異常

在每個應用程序中,都有一點應該吞下異常。例如,大多數時候,在Web應用程序中,我們不希望用戶看到異常錯誤。我們希望向用戶展示一些通用信息,並保留異常信息以用於調試。在這種情況下,我們可以將代碼塊包裝在try-catch塊中。如果發生錯誤,我們將捕獲異常,並記錄錯誤,並將一些通用信息返回給用戶,例如下面的代碼。

[HttpPost]
public async Task<IActionResult> UpdatePost(PostViewModel viewModel)
{
    try
    {
         _mediator.Send(new UpdatePostCommand{ PostViewModel = viewModel});
         return View(viewModel);
    }
    catch (Exception e)
    {
        _logger.LogError(e);
       return View(viewModel);
    }
}

請注意,在這種情況下,我們仍然會記錄錯誤,但是錯誤不會使圖層冒泡。因為這是最后一層,所以實際上上面沒有任何層。換句話說,應該使用異常而不是重新拋出異常的唯一代碼應該是UI或公共API。一些開發人員傾向於配置某種全局方式來處理在此層中發生的異常。您可以看一下我以前使用這種技術的帖子。當涉及到異常處理時,最重要的事情是異常處理永遠都不應隱藏問題。

最后

Final塊用於清除try塊中使用的任何剩余資源,而無需等待運行時的垃圾收集器完成該對象。我們可以使用它來關閉數據庫連接,文件流等。請注意,FileStream 在調用close之前,我們首先檢查object是否為null。不這樣做,finally塊可能會拋出自己的異常,這根本不是一件好事。

FileStream file = null;
var fileinfo = new FileInfo("C:\\file.txt");
try
{
    file = fileinfo.OpenWrite();
    file.WriteByte(0xF);
}
finally
{
    // Check for null because OpenWrite might have failed.
    if (file != null)
    {
        file.Close();
    }
}

我們可以在有或沒有catch塊的情況下使用它,重要的是,無論是否發生異常,總是總是要執行塊。另一個重要的一點是,由於數據庫連接之類的資源非常昂貴,因此最好盡快在finally塊中關閉它們,而不是等待垃圾收集器為我們完成它。using由於FileStream is正在實施,因此我們也可以使用該語句IDisposable。值得一提的是,using語句只是一種語法糖,可以轉換為嘗試並最終阻止,並且更具可讀性,並且總體而言是更好的選擇。

如有哪里講得不是很明白或是有錯誤,歡迎指正
如您喜歡的話不妨點個贊收藏一下吧


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM