原文網址:點選開啟連結
本文翻譯自CodeProject上的一篇文章,原文網址。
目錄
介紹
可信任的設備:相機、滑鼠以及鍵盤
「寫入操作」同樣可能失效
不要將重要的異常訊息儲存在Message屬性中
每個執行緒要包含一個try/catch區塊
只記錄Exception.Message的值,也需要記錄Exception.ToString()
要捕捉特定的異常
區塊中
不要忘記使用using
不要使用特殊返回值去表示方法中發生的異常
不要使用特殊返回值去表示方法中發生的異常
不要使用特殊返回值去表示方法中發生的異常
不要將「拋出異常」作為函數執行結果的一種
可以使用「拋出異常」的方式去著重說明
清空了堆疊追蹤(stack trace)資訊異常類應標記為Serializable
使用」拋出異常”錯誤.Assertert
使用」。個構造方法
不要重複造輪子
VB.NET
語中的詞不要使用非結構化異常處理(上 Error goto)
總結
介紹
你們相信嗎?我幾乎可以肯定所有人都會大喊我是個騙子。 「軟體程式幾乎不可能沒有bug!」
換句話說,我的意思是指軟體比較穩定。
軟體中有bug是可以理解的。但是如果是經常出現的bug,並且因為沒有足夠的提示訊息導致你無法迅速修復它,那麼這種情況是不可原諒的。
「更新客戶資料失敗,請與系統管理員聯絡然後重試」。
注意在這篇文章中,我主要講怎樣更好地處理.NET編程中的異常,並沒有打算討論怎樣顯示合適的“錯誤提示信息”,因為我覺得這個工作屬於UI界面開發者,並且它大部分依賴UI介面類型以及最終使用軟體的使用者。例如一個面向一般使用者的文字編輯器的「錯誤提示訊息」應該完全不同於一個Socket通訊框架,因為後者直接使用者是程式設計師。 做最壞的打算 遵守一些基本的設計原則可以讓你的程式更加健全,並且當錯誤發生時,能夠提升使用者體驗。我這裡說到的「提升用戶體驗」並不是指錯誤的提示窗體能夠讓用戶高興,而是指發生的錯誤不會損壞原有數據,不會讓整個電腦崩潰。如果你的程式遇到硬碟不足的錯誤,但是程式不會造成其他任何負面效果(僅僅提示錯誤訊息,不會引起其他問題,譯者註),那麼這時候就提升了用戶體驗。 提前檢查和驗證是避免bug發生的強大方法。你越早發現問題,就越早修復問題。幾個月後再想搞清楚「為什麼InvoiceItems表格中的ProductID欄會存在一個CustomerID資料?」是一件不太容易且相當惱火的事情。如果你使用一個類別來代替基本型別(如int、string)去儲存客戶(Customer)的資料的話,編譯器就不會允許剛才那件事情(指將CustomerID和ProductID混淆,譯者註)發生。 不要信任外部資料 外部資料是不可靠的,我們的軟體程式在使用它們之前必須嚴格檢查。無論這些外部資料來自於註冊表、資料庫、硬碟、socket或你用鍵盤編寫的文件,所有這些外部資料在使用前都必須嚴格檢查。很多時候,我看到一些程式完全信任配置文件,因為開發這些程式的程式設計師總是認為沒有人會編輯設定檔並損壞它。 可信任的設備:相機、滑鼠以及鍵盤 當你需要用到外部資料時,你可能會遇到以下狀況安全權限 3)資料不存在 4)資料不完整 〜 、socket套接字、資料庫、Web服務或串列口,以上情況均可能發生。所有的外部資料總會有失效的可能。
「寫入操作」同樣可能失效
不可信任的資料來源同樣也是一種不可信的資料倉儲。當你儲存資料時,相似情況可能會發生: 1)沒有足夠的安全權限 2)設備不存在 2)設備不存在的空間設備發生了物理錯誤 這就是為什麼一些壓縮軟體在工作時創建了一個臨時文件,當工作完成後再重命名,而不是直接修改源文件。原因是如果硬碟損壞(或軟體異常)可能導致原始資料遺失。 (譯者遇見過這種情況,備份數據時斷電,結果原來的舊版備份被損壞了,譯者註) 安全編程 我的一個朋友告訴我:一個好的程式設計師從來不會在他的程式中寫出糟糕的程式碼。我覺得這只是成為一個好程式設計師的必要條件而不是充分條件。下面我整理了一些當你進行異常處理時,可能會寫的「糟糕程式碼」:
不要拋出「new Exception()」
請不要這樣做。 Exception是一個非常抽象的異常類,捕捉這類異常通常會產生許多負面影響。通常情況下應該定義我們自己的異常類,並且需要區分系統(framework)拋出的異常和我們自己拋出的異常。
不要將重要的異常資訊儲存在Message屬性中
異常都封裝在類別中。當你需要傳回異常訊息時,請將資訊儲存在一些單獨的屬性中(而不要放在Message屬性中),否則人們很難從Message屬性中解析出他們需要的資訊。例如當你只需要修正拼字錯誤,如果你將錯誤訊息和其它提示內容一起以String的形式寫在了Message屬性中,那麼別人該怎麼簡單地取得他們要的錯誤訊息呢?你很難想像他們要做多少努力。
每個執行緒要包含一個try/catch區塊
一般異常處理都放在了程式中一個比較集中的地方。每個執行緒都需要有一個try/catch區塊,否則你會漏掉某些異常而出現難以理解的問題。當一個程式開啟了多個執行緒去處理後台任務時,通常你會建立一個類型來儲存各個執行緒執行的結果。這時候請不要忘記了為類型增加一個欄位來儲存每個執行緒可能發生的異常,否則的話,主執行緒不會知道其他執行緒的異常情況。在一些「即發即忘」的場合(意思是主執行緒開啟執行緒後不再關心執行緒的運行情況,譯者註),你可能需要將主執行緒中的異常處理邏輯複製一份到你的子執行緒中去。
捕獲異常後要記錄下來
不管你的程式是使用何種方式記錄文字——log4net、EIF、Event Log、TracesListers 。重要的是:當你遇到異常後,應該在某個地方將它記錄在日誌中。但是請僅記錄一次,否則的話,你最後會得到一個非常大的日誌文件,包含了許多重複資訊。
不要只記錄Exception.Message的值,還需要記錄Exception.ToString()
當談到我們記錄日誌時,不要談到我們記錄日誌時,不要當談到我們記錄日誌時,不要談到我們記錄了的值,而不是Exception.Message。因為Exception.ToString()包含了「堆疊追蹤」(stack trace)訊息,內部異常訊息以及Message。通常這些資訊非常重要,如果你只記錄Exception.Message的話,你只可能看到類似「物件參考未指向堆中實例」這樣的提示。
要捕獲具體的異常
如果你要捕獲異常,請盡可能的捕獲具體異常(而非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,所以会提示“数据不合法”。
不要中止异常上抛
最坏的情况是,你编写catch(Exception)这样的代码,并且在catch块中啥也不干。请不要这样做。
清理代码要放在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 { string fileContents; using (StreamReader sr = new StreamReader(FileName)) { fileContents = sr.ReadToEnd(); } File.Delete(FileName); return fileContents; } catch (Exception) { File.Delete(FileName); throw; } }
变量fileContents去哪里了?它不再需要了,因为返回点在清理代码前面。这是让代码在方法返回后才执行的好处:你可以清理那些返回语句需要用到的资源(方法返回时需要用到的资源,所以资源只能在方法返回后才能释放,译者注)。
不要忘记使用using
仅仅调用对象的Dispose()方法是不够的。即使异常发生时,using关键字也能够防止资源泄漏。(关于对象的Dispose()方法的用法,可以关注我的书,有一章专门介绍。译者注)
不要使用特殊返回值去表示方法中发生的异常
因为这样做有很多问题:
1)直接抛出异常更快,因为使用特殊的返回值表示异常时,我们每次调用完方法时,都需要去检查返回结果,并且这至少要多占用一个寄存器。降低代码运行速度。
2)特殊返回值能,并且很可能被忽略
3)特殊返回值不能包含堆栈跟踪(stack trace)信息,不能返回异常的详细信息
4)很多时候,不存在一个特殊值去表示方法中发生的异常,比如,除数为零的情况:
public int pide(int x, int y) { return x / y; }
不要使用“抛出异常”的方式去表示资源不存在
微软建议在某些特定场合,方法可以通过返回一些特定值来表示方法在执行过程中发生了预计之外的事情。我知道我上面提到的规则恰恰跟这条建议相反,我也不喜欢这样搞。但是一些API确实使用了某些特殊返回值来表示方法中的异常,并且工作得很好,所以我还是觉得你们可以谨慎地遵循这条建议。
我看到了.NET Framework中很多获取资源的API方法使用了特殊返回值,比如Assembly.GetManifestStream方法,当找不到资源时(异常),它会返回null(不会抛出异常)。
不要将“抛出异常”作为函数执行结果的一种
这是一个非常糟糕的设计。代码中包含太多的try/catch块会使代码难以理解,恰当的设计完全可以满足一个方法返回各种不同的执行结果(绝不可能到了必须使用抛出异常的方式才能说明方法执行结果的地步,译者注),如果你确实需要通过抛出异常来表示方法的执行结果,那只能说明你这个方法做了太多事情,必须进行拆分。(这里原文的意思是,除非确实有异常发生,否则一个方法不应该仅仅是为了说明执行结果而抛出异常,也就是说,不能无病呻呤,译者注)
可以使用“抛出异常”的方式去着重说明不能被忽略的错误
我可以举个现实中的例子。我为我的Grivo(我的一个产品)开发了一个用来登录的API(Login),如果用户登录失败,或者用户并没有调用Login方法,那么他们调用其他方法时都会失败。我在设计Login方法的时候这样做的:如果用户登录失败,它会抛出一个异常,而并不是简单的返回false。正因为这样,调用者(用户)才不会忽略(他还没登录)这个事实。
不要清空了堆栈跟踪(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捕获的同一个异常。
拓展阅读:
C# 异常处理(Catch Throw)IL分析
异常类应标记为Serializable
很多时候,我们的异常需要能被序列化。当我们派生一个新的异常类型时,请不要忘了给它加上Serializable属性。谁会知道我们的异常类会不会用在Remoting Call或者Web Services中呢?
使用”抛出异常”代替Debug.Assert
当我们发布程序后,不要忘了Debug.Assert将会被忽略。我们在代码中做一些检查或者验证工作时,最好使用抛出异常的方式代替输出Debug信息。
将输出Debug信息这种方式用到单元测试或者那些只需要测试当软件真正发布后确保不会出错的场合。
每个异常类至少包含三个构造方法
做这件事相当简单(直接从其他的类型粘贴拷贝相同的代码即可),如果你不这样做,那么别人在使用你编写的异常类型时,很难遵守上面给出的一些规则的。
我指的哪些构造方法呢?这三个构造方法可以参见这里。
不要重复造轮子
已经有很多在异常处理方面做得比较好的框架或库,微软提供的有两个:
Exception Management Application Block
Microsoft Enterprise Instrumentation Framework
注意,如果你不遵守我上面提到的一些規則,這些庫對你來講可能沒什麼用。
VB.NET
如果你已經讀完整篇文章,你會發現所有的範例程式碼都是用C#寫的範例。那是因為C#是我比較喜歡的.NET語言,而VB.NET有它自己的一些特殊規則。
模擬C#中的using語句
不幸的是,VB.NET中並沒有using語句。你每次在釋放一個物件的非託管資源時,不得不這樣去做:
如果你不按照上面那種方式調用DIspose方法的話,很可能會出現錯誤(有關Dispose方法的調用,請關注新書。
不要使用非結構化異常處理(On Error Goto)
·非結構化拉)在1974年說過“goto語句有害無益”,這已經是30年之前了!請刪除你程式碼中的所有goto式的語句,我向你保證,他們萬害無一益。 (艾茲赫爾·戴克斯特拉提出了「goto有害論」、信號量和PV原語,解決了有趣的哲學家就餐問題。《軟體故事》一書中講Fortran語言時提到過他。處理的開始,並讓我們寫的程式更加健壯。
譯者話:
我有一個缺點,不知道有沒有網友跟我一樣。我是個慢熱型的人,對科技也一樣,好多東西流行顛峰時期過去了我才開始有所感覺。主要一是因為我對新鮮東西不太感冒;二是我總覺得原來學習的東西還沒掌握好就換,有點半途而廢的意思。其實我也知道這樣非常不好,畢竟IT產業是個快速發展的產業,一沒跟上腳步就落後了。
正是遇見這樣相互矛盾的情況,我在學習知識的時候都是重點學習技術間的通性,所謂通性,即十年、二十年甚至三十年不太會變、不太會沒落的東西,如果你現在從事的公司實際開發過程中一直使用某一套框架,你要是死抓著“怎樣使用這個框架做出好的系統”不放,那麼過幾年你可能就落伍了。而如果你研究研究程式設計中的共通性,例如協定、系統間的互動原理等,這些在每個網路通訊系統中都會用到,無論是貌似已經過時了的PC程序,還是Web程序,還是當前流行的移動APP,都會用到,而且基本原理都是一樣的。看得多了,就發現新東西出來好像是換湯不換藥的感覺(稍微誇張:-))
因此,我給那些跟我一樣,不太跟隨新鮮事物的人、或者那些長期從事某一類固定開發工作的人的建議是:找準技術間的共通性,不要停留在技術表面,除非你對新鮮事物夠感興趣,並且有充分精力。
以上這些話也是我們公司開討論會時分享的。
作者:週見智
出處:http://www.php.cn/本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段落聲明,且在文章頁面明顯位置給予原文連接,否則保留追究法律責任的權利。
關於CLR的 「兩輪遍歷」異常處理策略。
當應用程式擁有多層嵌套的異常捕獲結構時,如果最底層(其實在中間層也一樣)發生了異常,CLR將優先在引發異常的那一層去搜索catch語句塊,看看有沒有“相容”
此類型異常的處理代碼,如果沒有,就“跳到”上一層去搜索,如果上一層還沒有,繼續搜索上一層的“上一層”,由此直到應用程序的最頂層。
這是CLR處理巢狀異常捕獲結構應用程式的「第一輪」遍歷-----尋找合適的異常處理程序。
如果在某一層找到了異常處理程序,注意,CLR並不會馬上執行之,而是回到"事故現場",再次進行“第二輪”遍歷,執行所有“中間”層次的finally 語句塊,然後,執行
找到異常處理程序,最後,再從本層開始一直遍歷到最頂層,執行所有的finally語句區塊。
以上就是.NET中異常處理的最佳實踐(譯)的內容,更多相關內容請關注PHP中文網(www.php.cn)!