C#異常處理18條最佳實踐


首先,異常處理應該是系統設計規約的一部分出現在系統設計文檔中,而不僅僅是一種技術實現。

作為設計文檔的一部分,異常處理應該着眼於系統容錯性和穩定性(正如樓主提到的那樣)。然后在根據這個規約,再來具體討論和選擇異常處理中使用的各種技術細則。
比如,在設計服務時,必須在服務的調用接口處有異常處理,否則客戶端傳過來的任何有害數據都可能讓服務器掛掉。

比如,對異常的處理在系統的設計中,必須有明確說明,不能隨便在哪個模塊中處理異常。

軟件中有bug是可以理解的。但是如果是經常出現的bug,並且因為沒有足夠的提示信息導致你不能迅速修復它,那么這種情況是不可被原諒的。

為了更好地理解我上面所說的話,我舉個例子:我經常看見無數的商業軟件在遇到硬盤不足時給出這樣的錯誤提示:

“更新客戶資料失敗,請與系統管理員聯系然后重試”。

除了這些外,其他任何信息都沒有被記錄。要搞清楚到底什么原因引起的這個錯誤是一件非常耗時的過程,在真正找到問題原因之前,程序員可能需要做各種各樣的猜測。

下面就來說說C#異常處理18條最佳實踐

1. 不要拋出“new Exception()”

請別這樣做。Exception是一個非常抽象的異常類,捕獲這類異常通常會產生很多負面影響。通常情況下應該定義我們自己的異常類,並且需要區分系統(framework)拋出的異常和我們自己拋出的異常。

2. 不要將重要的異常信息存儲在Message屬性中

異常都封裝在類中。當你需要返回異常信息時,請將信息存儲在一些單獨的屬性中(而不要放在Message屬性中),否則人們很難從Message屬 性中解析出他們需要的信息。比如當你僅僅需要糾正一下拼寫錯誤,如果你將錯誤信息和其它提示內容一起以String的形式寫在了Message屬性中,那 么別人該怎樣簡單地獲取他們要的錯誤信息呢?你很難想象到他們要做多少努力。

3. 每個線程要包含一個try/catch塊

一般異常處理都放在了程序中一個比較集中的地方。每個線程都需要有一個try/catch塊,否則你會漏掉某些異常從而出現難以理解的問題。當一個 程序開啟了多個線程去處理后台任務時,通常你會創建一個類型來存儲各個線程執行的結果。這時候請不要忘記了為類型增加一個字段來存儲每個線程可能發生的異 常,否則的話,主線程不會知道其他線程的異常情況。在一些“即發即忘”的場合(意思主線程開啟線程后不再關心線程的運行情況,譯者注),你可能需要將主線 程中的異常處理邏輯復制一份到你的子線程中去。

4. 捕獲異常后要記錄下來

不管你的程序是使用何種方式記錄日志——log4net、EIF、Event Log、TraceListeners或者文本文件等,這些都不重要。重要的是:當你遇到異常后,應該在某個地方將它記錄在日志中。但是請僅僅記錄一次, 否則的話,你最后會得到一個非常大的日志文件,包含了許多重復信息。

5. 不要只記錄Exception.Message的值,還需要記錄Exception.ToString()

當我們談到記錄日志時,不要忘了我們應該記錄Exception.ToString()的值,而不是Exception.Message。因為 Exception.ToString()包含了“堆棧跟蹤”(stack trace)信息,內部異常信息以及Message。通常這些信息非常重要,而如果你只記錄Exception.Message的話,你只可能看到類似 “對象引用未指向堆中實例”這樣的提示。

6. 要捕獲具體的異常

如果你要捕獲異常,請盡可能的捕獲具體異常(而非Exception)。

我經常看見初學者說,一段好的代碼就是不能拋出異常的代碼。其實這說法是錯誤的,好的代碼在必要時應該拋出相應的異常,並且好的代碼只能捕獲它知道該怎么處理的異常(注意這句話,譯者注)。

下面的代碼作為對這條規則的說明。我敢打賭編寫下面這段代碼的那個家伙看見了會殺了我的,但是它確實是摘取自真實編程工作中的一段代碼。

第一個類MyClass在一個程序集中,第二個類GenericLibrary在另一個程序集中。在開發的機器上運行正常,但是在測試機器上卻總是拋出“數據不合法!”的異常,盡管每次輸入的數據都是合法的。

你們能說說這是為什么嗎?

public class MyClass
{
    public static string ValidateNumber(string userInput)
    {
        try
        {
            int val = GenericLibrary.ConvertToInt(userInput);
            return "Valid number";
        }
        catch (Exception)
        {
            return "Invalid number";
        }
    }
}
 
public class GenericLibrary
{
    public static int ConvertToInt(string userInput)
    {
        return Convert.ToInt32(userInput);
    }
}

這個問題的原因就是異常處理不太具體。根據MSDN上的介紹,Convert.ToInt32方法僅僅會拋出ArgumentException、FormatException以及OverflowException三個異常。所以,我們應該僅僅處理這三個異常。

問題發生在我們程序安裝的步驟上,我們沒有將第二個程序集(GenericLibrary.dll)打包進去。所以程序運行 后,ConvertToInt方法會拋出FileNotFoundException異常,但是我們捕獲的異常是Exception,所以會提示“數據不 合法”。

7. 不要中止異常上拋

最壞的情況是,你編寫catch(Exception)這樣的代碼,並且在catch塊中啥也不干。請不要這樣做。

8. 清理代碼要放在finally塊中

大多數時候,我們只處理某一些特定的異常,其它異常不負責處理。那么我們的代碼中就應該多一些finally塊(就算發生了不處理的異常,也可以在finally塊中做一些事情,譯者注),比如清理資源的代碼、關閉流或者回復狀態等。請把這當作習慣。

有一件大家容易忽略的事情是:怎樣讓我們的try/catch塊同時具備易讀性和健壯性。舉個例子,假設你需要從一個臨時文件中讀取數據並且返回一個字符串。無論什么情況發生,我們都得刪除這個臨時文件,因為它是臨時性的。

讓我們先看看最簡單的不使用try/catch塊的代碼:

string ReadTempFile(string FileName)
{
    string fileContents;
    using (StreamReader sr = new StreamReader(FileName))
    {
        fileContents = sr.ReadToEnd();
    }
    File.Delete(FileName);
    return fileContents;
}

這段代碼有一個問題,ReadToEnd方法有可能拋出異常,那么臨時文件就無法刪除了。所以有些人修改代碼為:

string ReadTempFile(string FileName)
{
    try
    {
        string fileContents;
        using (StreamReader sr = new StreamReader(FileName))
        {
            fileContents = sr.ReadToEnd();
        }
        File.Delete(FileName);
        return fileContents;
    }
    catch (Exception)
    {
        File.Delete(FileName);
        throw;
    }
}

這段代碼變得復雜一些,並且它包含了重復性的代碼。

那么現在讓我們看看更簡介更健壯的使用try/finally的方式:

string ReadTempFile(string FileName)
{
    try
    {
        using (StreamReader sr = new StreamReader(FileName))
        {
            return sr.ReadToEnd();
        }
    }
    finally
    {
        File.Delete(FileName);
    }
}

變量fileContents去哪里了?它不再需要了,因為返回點在清理代碼前面。這是讓代碼在方法返回后才執行的好處:你可以清理那些返回語句需要用到的資源(方法返回時需要用到的資源,所以資源只能在方法返回后才能釋放,譯者注)。

9. 不要忘記使用using

僅僅調用對象的Dispose()方法是不夠的。即使異常發生時,using關鍵字也能夠防止資源泄漏。

10.不要使用特殊返回值去表示方法中發生的異常

因為這樣做有很多問題:

1)直接拋出異常更快,因為使用特殊的返回值表示異常時,我們每次調用完方法時,都需要去檢查返回結果,並且這至少要多占用一個寄存器。降低代碼運行速度。

2)特殊返回值能,並且很可能被忽略

3)特殊返回值不能包含堆棧跟蹤(stack trace)信息,不能返回異常的詳細信息

4)很多時候,不存在一個特殊值去表示方法中發生的異常,比如,除數為零的情況:

public int divide(int x, int y)
{
    return x / y;
}

11. 不要使用“拋出異常”的方式去表示資源不存在

微軟建議在某些特定場合,方法可以通過返回一些特定值來表示方法在執行過程中發生了預計之外的事情。我知道我上面提到的規則恰恰跟這條建議相反,我 也不喜歡這樣搞。但是一些API確實使用了某些特殊返回值來表示方法中的異常,並且工作得很好,所以我還是覺得你們可以謹慎地遵循這條建議。

我看到了.NET Framework中很多獲取資源的API方法使用了特殊返回值,比如Assembly.GetManifestStream方法,當找不到資源時(異常),它會返回null(不會拋出異常)。

12. 不要將“拋出異常”作為函數執行結果的一種

這是一個非常糟糕的設計。代碼中包含太多的try/catch塊會使代碼難以理解,恰當的設計完全可以滿足一個方法返回各種不同的執行結果(絕不可 能到了必須使用拋出異常的方式才能說明方法執行結果的地步,譯者注),如果你確實需要通過拋出異常來表示方法的執行結果,那只能說明你這個方法做了太多事 情,必須進行拆分。

13. 可以使用“拋出異常”的方式去着重說明不能被忽略的錯誤

我可以舉個現實中的例子。我為我的Grivo(我的一個產品)開發了一個用來登錄的API(Login),如果用戶登錄失敗,或者用戶並沒有調用 Login方法,那么他們調用其他方法時都會失敗。我在設計Login方法的時候這樣做的:如果用戶登錄失敗,它會拋出一個異常,而並不是簡單的返回 false。正因為這樣,調用者(用戶)才不會忽略(他還沒登錄)這個事實。

14.不要清空了堆棧跟蹤(stack trace)信息

堆棧跟蹤信息是異常發生時最重要的信息,我們經常需要在catch塊中處理一些異常,有時候還需要重新上拋異常(re-throw)。下面來看看兩種方法(一種錯誤的一種正確的):

錯誤的做法:

try
{
    // Some code that throws an exception
}
catch (Exception ex)
{
    // some code that handles the exception
    throw ex;
}

為什么錯了?因為當我們檢查堆棧跟蹤信息時,異常錯誤源變成了“thorw ex;”,這隱藏了真正異常拋出的位置。試一下下面這種做法:

try
{
    // Some code that throws an exception
}
catch (Exception ex)
{
    // some code that handles the exception
    throw;
}

有什么變化沒?我們使用“throw;”代替了“throw ex;”,后者會清空原來的堆棧跟蹤信息。如果我們在拋出異常時沒有指定具體的異常(簡單的throw),那么它會默認地將原來捕獲的異常繼續上拋。這樣 的話,上層代碼捕獲的異常還是最開始我們通過catch捕獲的同一個異常。

15.異常類應標記為Serializable

很多時候,我們的異常需要能被序列化。當我們派生一個新的異常類型時,請不要忘了給它加上Serializable屬性。誰會知道我們的異常類會不會用在Remoting Call或者Web Services中呢?

16.使用”拋出異常”代替Debug.Assert

當我們發布程序后,不要忘了Debug.Assert將會被忽略。我們在代碼中做一些檢查或者驗證工作時,最好使用拋出異常的方式代替輸出Debug信息。

將輸出Debug信息這種方式用到單元測試或者那些只需要測試當軟件真正發布后確保不會出錯的場合。

17.不要重復造輪子

已經有很多在異常處理方面做得比較好的框架或庫,微軟提供的有兩個:

Exception Management Application Block

Microsoft Enterprise Instrumentation Framework

注意,如果你不遵守我上面提到的一些規則,這些庫對你來講可能沒什么用。

18.不要信任外部數據

外部數據是不可靠的,我們的軟件程序在使用它們之前必須嚴格檢查。無論這些外部數據來自於注冊表、數據庫、硬盤、socket還是你用鍵盤編寫的文 件,所有這些外部數據在使用前必須嚴格進行檢查。很多時候,我看到一些程序完全信任配置文件,因為開發這些程序的程序員總是認為沒有人會編輯配置文件並損 壞它。

強類型檢查和驗證是避免bug發生的有力方法。你越早發現問題,就越早修復問題。幾個月后再想搞清楚“為什么InvoiceItems表中的 ProductID欄會存在一個CustomerID數據?”是一件不太容易並且相當惱火的事情。如果你使用一個類代替基本類型(如int、 string)去存儲客戶(Customer)的數據的話,編譯器就不會允許剛才那件事情(指將CustomerID和ProductID混淆)


免責聲明!

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



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