[.net] 關於Exception的幾點思考和在項目中的使用(三)

本文鏈接: //www.cnblogs.com/hubaijia/p/about-exceptions-3.html

系列文章:

本文目錄

Web Api 的錯誤返回

在使用.net 的 Web Api構建Endpoint對外提供訪問時,往往需要統一的錯誤返回格式。

如果按照前面兩篇文章(一)、(二)所說,採用帶有ErrorCode的異常ErrorCodeException,那麼在WebApi中返回錯誤時,只要返回ErrorCode即可。

程式碼如下:

[HttpPut]
[Authorize]
public IActionResult Update(UpdatetNickNameRequest request)
{
    try
    {
        long userId = User.GetUserId();

        _identityService.SetNickName(userId, request.NickName);
        
        return Ok();
    }
    catch (IdentityException ex)
    {
        return new BadRequestObjectResult(ex.ErrorCode);
    }
}

ErrorCode類包含了Id, Name, Message屬性,方便客戶端在收到錯誤返回後進行處理。

自定義的客戶端在接受到Api的返回後,檢查HttpStatusCode,如果不成功(不是2xx),那麼也直接拋出帶有ErrorCode的異常ErrorCodeException即可。

這樣,前後端的處理就變得一致和簡便。特別是如果你在前端也採用 .net 技術,比如 blazor、xamarin、wpf、winform等等,那前後端可以使用相同的CodeBase。

Exception的捕捉

前面幾篇文章,一直在關注Exception的拋出,現在我們來關注一下Exception的捕捉。

那些基礎的catch/finally這裡就不再贅述,說幾個項目中實際出現的現象和問題。

不要吃掉異常

不要吃掉異常有兩個層面,一個是直接忽略,比如catch了所有的Exception,並不妥善處理(假處理,就列印一句log,甚至不處理),不過這種失誤要不是新手所犯,要不就是編碼過程中草草了事的結果。

這裡主要注意第二個層面,即catch時不要隱藏異常的類型,即不要catch(Exception ex),這意味著隱藏了異常的類型,吃掉了針對不同的錯誤採取不同的措施的機會。

微軟的code analysis也直接給出了規則 CA1031:不要捕捉一般異常類型.

Checked Exception

CE是確保你所有的異常都得到捕捉,它往往在編譯層面提供對Exception的檢查,確保你的程式不會因為未處理的異常而終止。

在編寫可靠的程式時,程式設計師需要知道調用的方法拋出什麼異常,我是否需要處理,還是包裝,還是直接拋出不理,總之我們需要有途徑獲取這樣的資訊。

當然,Checked Exception肯定會帶來更多的程式碼量,且在項目初始建立起來,往往需要修改一處,而動整個鏈條上的程式碼,一層層的修改。

所以,有些人喜歡CE,認為它帶來了規矩,改善了團隊程式碼品質;當然也有些人認為它帶來了繁瑣。

仁者見仁智者見智,這裡不加入這些爭辯。爭辯1, 爭辯2, 爭辯3, 爭辯4

.net vs java

不得不拿這兩門語言進行對比,每個世界都要互相借鑒。

熟悉java語法的同學肯定會知道java方法定義上有throws這一關鍵詞:

public int div(int i,int j) throws Exception {...}

throws這一關鍵詞保證了從方法定義上就能知道一個方法拋出什麼樣的異常,直接藉助編譯器檢查或者IDE的智慧提醒,就不會漏掉異常。

而c#中並沒有相同的實現,可以見Stackoverflow上的討論.

那麼在 .net 世界中當你調用一個方法時,怎麼妥善的知道這個方法拋出什麼樣的異常呢?知道後我們才能決定是否處理這個異常,還是繼續拋出。

目前的答案是:注釋!(不要笑,很嚴肅的解決方案)。

/// <exception cref="IdentityService.IdentityService">這樣寫Exception注釋</exception>
public void SetNickName(long userId, string newNickName)
{
    //....
    Ensure.NickNameNotExisted(newNickName);
    //....
}

當你翻看.net的源碼時,會看到所有方法的注釋中都良好的列出了有哪些異常。

有幾個問題:

  • 我們團隊沒人寫注釋,怎麼辦?
  • 調用方法時,並沒有智慧提示有哪些異常,所以我們經常忽略
  • 想看異常就得F12看定義,太繁瑣。

有很多問題只從技術上沒法解決,但我們盡量可以藉助一些Review工具來檢查團隊的程式碼,提出要求。

此外你會喜歡上 ctrl + k, ctrl+i 這個快捷鍵的,他能幫助你快速查看注釋文檔,查看有哪些異常。

visual studio 擴展推薦

在這裡,我推薦一個visual studio的擴展,是的,它的名字就叫 Checked Exceptions, 這是我必備的一款擴展。

c0EGLt.png

這款插件會藉助注釋的形式,協助實現Checked Exception的功能,並且可以快速添加相應注釋。

這個有個小提示:如果你從項目伊始採用這款插件,折磨小一點,如果半路使用,那麼當作檢驗團隊程式碼強健性的工具也不錯。

此外,這個款擴展可能還有些bug,導致即使注釋了Exception還不斷提示,所以我平時並不一直啟用它,而是在做Code Review時,使用它作為一個檢查工具。

這樣可以比較好的解決上面提到的Checked Exception的缺點,又利用它的優點。

常見模式

不要重複的拋出

這個只是簡單提醒下,見如下程式碼。

void BadSmellMethod()
{
    try
    {
        .....
    }
    catch(Exception ex)
    {
        //.... some thing
        
        // 錯誤的做法
        throw ex;  
    }
}

void GoodMethod()
{
    try
    {
        .....
    }
    catch
    {
        //... some thing
        // 錯誤的做法
        throw;  
    }
}

簡單來說就是重複拋出,丟失了引發異常原始方法和當前方法調用之間的StackTrace。

在code analysis中也有相應規則。CA2200.

在asp.net core 中

在捕捉異常時,往往一個異常被一路拋出,或者包裝再拋出,直到終點。如果到了終點還沒有被捕獲,那麼就會引發程式中止,這是誰都不想看到的。

在asp.net core中,這個終點就是Controller控制器,所以我們需要在Controller的方法里調用需要的Service,然後兜住所有的可能的異常,這也是可以直接捕獲Exception的地方。

程式碼如下:

[HttpPut]
[Authorize]
public IActionResult Update(UpdatetNickNameRequest request)
{
    try
    {
        long userId = User.GetUserId();

        _identityService.SetNickName(userId, request.NickName);

        return Ok();
    }
    catch (IdentityException ex)
    {
        // 具體異常
        //...others
        return new BadRequestObjectResult(ex.ErrorCode);
    }
    catch (OtherException ex)
    {
        //具體異常
        //... others
        return new BadRequestObjectResult(ex.ErrorCode);
    }
    catch(Exception ex)
    {
        //這裡兜住其他未發現的一切異常
        //做好日誌
        //分析後再優化處理
        return new BadRequestObjectResult(ErrorCode.Empty);
    }
}

在xamarin.forms中

如果你是同道中人,使用xamarin.forms,那麼你肯定知道MVVM模式。

異常的終點往往就在MVVM模式中的ViewModel中,比如LoginPageViewModel中,同樣ViewModel調用各項Service,你需要在這裡兜住所有可能出現的異常。

結語

本文,簡要介紹了具體項目中異常的捕捉問題,歡迎大家交流指正。

下篇,我們關注一下 非同步編程中的Exception,以及全局錯誤處理。

謝謝閱讀。