原文地址:點擊開啟連結
本文翻譯自CodeProject上的一篇文章,原文地址。
目錄
介紹
做最壞的打算
提前檢查
不要信任外部資料
可信任的裝置:網路攝影機、滑鼠以及鍵盤
“寫操作”同樣可能失效
安全編程
不要重複造輪子
VB.NET
總結
介紹
“我的軟體程式從來都不會出錯”。你們相信嗎?我幾乎可以肯定所有人都會大喊我是個騙子。“軟體程式幾乎不可能沒有bug!”
事實上,開發一個可信任、健全的軟體程式並不是不可能的事情。注意我這裡並不是指那些用於控制核電站的軟體,而是指一些常見的商業軟體,這些軟體可能運行在伺服器上,又或者PC機上,它們可以連續工作幾個星期甚至幾個月都不會出現重大問題。可以猜到,我剛才的意思是指軟體有一個比較低的出錯率,你可以迅速找到出錯的原因並快速修複,並且出現的錯誤並不會造成重大的資料損毀。
換句話說,我的意思是指軟體比較穩定。
軟體中有bug是可以理解的。但是如果是經常出現的bug,並且因為沒有足夠的提示資訊導致你不能迅速修複它,那麼這種情況是不可被原諒的。
為了更好地理解我上面所說的話,我舉個例子:我經常看見無數的商業軟體在遇到硬碟不足時給出這樣的錯誤提示:
“更新客戶資料失敗,請與系統管理員聯絡然後重試”。
除了這些外,其他任何資訊都沒有被記錄。要搞清楚到底什麼原因引起的這個錯誤是一件非常耗時的過程,在真正找到問題原因之前,程式員可能需要做各種各樣的猜測。
注意在這篇文章中,我主要講怎樣更好地處理.NET編程中的異常,並沒有打算討論怎樣顯示合適的“錯誤提示資訊”,因為我覺得這個工作屬於UI介面開發人員,並且它大部分依賴於UI介面類型以及最終使用軟體的使用者。比如一個面向普通使用者的文字編輯器的“錯誤提示資訊”應該完全不同於一個Socket通訊架構,因為後者直接使用者是程式員。
做最壞的打算
遵守一些基本的設計原則可以讓你的程式更加健全,並且當錯誤發生時,能夠提升使用者體驗。我這裡說到的“提升使用者體驗”並不是指錯誤的提示表單能夠讓使用者高興,而是指發生的錯誤不會損壞原有資料,不會讓整個電腦崩潰。如果你的程式遇到硬碟不足的錯誤,但是程式不會造成其他任何負面效果(僅僅提示錯誤資訊,不會引起其他問題,譯者注),那麼這時候就提升了使用者體驗。
強型別檢查和驗證是避免bug發生的有力方法。你越早發現問題,就越早修複問題。幾個月後再想搞清楚“為什麼InvoiceItems表中的ProductID欄會存在一個CustomerID資料?”是一件不太容易並且相當惱火的事情。如果你使用一個類代替基本類型(如int、string)去儲存客戶(Customer)的資料的話,編譯器就不會允許剛才那件事情(指將CustomerID和ProductID混淆,譯者注)發生。
外部資料是不可靠的,我們的軟體程式在使用它們之前必須嚴格檢查。無論這些外部資料來自於註冊表、資料庫、硬碟、socket還是你用鍵盤編寫的檔案,所有這些外部資料在使用前必須嚴格進行檢查。很多時候,我看到一些程式完全信任設定檔,因為開發這些程式的程式員總是認為沒有人會編輯設定檔並損壞它。
當你需要用到外部資料時,你可能會遇到以下情況:
1)沒有足夠的安全許可權
2)資料不存在
3)資料不完整
4)資料完整,但是格式不對
不管資料來源是註冊表中的某個鍵、一個檔案、socket通訊端、資料庫、Web服務或者串口,以上情況均可能發生。所有的外部資料總會有失效的可能。
不可信任的資料來源同樣也是一種不可信任的資料倉儲。當你儲存資料時,相似情況依舊可能會發生:
1)沒有足夠的安全許可權
2)裝置不存在
3)沒有足夠的空間
4)存放裝置發生了物理錯誤
這就是為什麼一些壓縮軟體在工作時建立了一個臨時檔案,當工作完成後再重新命名,而不是直接修改源檔案。原因是如果硬碟損壞(或者軟體異常)可能導致未經處理資料丟失。(譯者遇見過這種情況,備份資料時斷電,結果原來的舊版備份被損壞了,譯者注)
安全編程
我的一個朋友告訴我:一個好的程式員從來不會在他的程式中編寫糟糕的代碼。我覺得這隻是成為一個好程式員的必要條件而不是充分條件。下面我整理了一些當你進行異常處理時,可能會編寫的“糟糕代碼”:
請別這樣做。Exception是一個非常抽象的異常類,捕獲這類異常通常會產生很多負面影響。通常情況下應該定義我們自己的異常類,並且需要區分系統(framework)拋出的異常和我們自己拋出的異常。
異常都封裝在類中。當你需要返回異常資訊時,請將資訊儲存在一些單獨的屬性中(而不要放在Message屬性中),否則人們很難從Message屬性中解析出他們需要的資訊。比如當你僅僅需要糾正一下拼字錯誤,如果你將錯誤資訊和其它提示內容一起以String的形式寫在了Message屬性中,那麼別人該怎樣簡單地擷取他們要的錯誤資訊呢?你很難想象到他們要做多少努力。
一般異常處理都放在了程式中一個比較集中的地方。每個線程都需要有一個try/catch塊,否則你會漏掉某些異常從而出現難以理解的問題。當一個程式開啟了多個線程去處理背景工作時,通常你會建立一個類型來儲存各個線程執行的結果。這時候請不要忘記了為類型增加一個欄位來儲存每個線程可能發生的異常,否則的話,主線程不會知道其他線程的異常情況。在一些“即發即忘”的場合(意思主線程開啟線程後不再關心線程的運行情況,譯者注),你可能需要將主線程中的異常處理邏輯複製一份到你的子線程中去。
不管你的程式是使用何種方式記錄日誌——log4net、EIF、Event Log、TraceListeners或者文字檔等,這些都不重要。重要的是:當你遇到異常後,應該在某個地方將它記錄在日誌中。但是請僅僅記錄一次,否則的話,你最後會得到一個非常大的記錄檔,包含了許多重複資訊。
當我們談到記錄日誌時,不要忘了我們應該記錄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塊中做一些事情,譯者注),比如清理資源的代碼、關閉流或者回複狀態等。請把這當作習慣。
有一件大家容易忽略的事情是:怎樣讓我們的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去哪裡了?它不再需要了,因為返回點在清理代碼前面。這是讓代碼在方法返回後才執行的好處:你可以清理那些返回語句需要用到的資源(方法返回時需要用到的資源,所以資源只能在方法返回後才能釋放,譯者注)。
僅僅調用對象的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。正因為這樣,調用者(使用者)才不會忽略(他還沒登入)這個事實。
堆疊追蹤資訊是異常發生時最重要的資訊,我們經常需要在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屬性。誰會知道我們的異常類會不會用在Remoting Call或者Web Services中呢?
當我們發布程式後,不要忘了Debug.Assert將會被忽略。我們在代碼中做一些檢查或者驗證工作時,最好使用拋出異常的方式代替輸出Debug資訊。
將輸出Debug資訊這種方式用到單元測試或者那些只需要測試當軟體真正發布後確保不會出錯的場合。
做這件事相當簡單(直接從其他的類型粘貼拷貝相同的代碼即可),如果你不這樣做,那麼別人在使用你編寫的異常類型時,很難遵守上面給出的一些規則的。
我指的哪些構造方法呢?這三個構造方法可以參見這裡。
不要重複造輪子
已經有很多在異常處理方面做得比較好的架構或庫,微軟提供的有兩個:
Exception Management Application Block
Microsoft Enterprise Instrumentation Framework
注意,如果你不遵守我上面提到的一些規則,這些庫對你來講可能沒什麼用。
VB.NET
如果你已經讀完整篇文章,你就會發現所有的範例程式碼都是用C#編寫的。那是因為C#是我比較喜歡的.NET語言,並且VB.NET有它自己的一些特殊規則。
不幸的是,VB.NET中並沒有using語句。你每次在釋放一個對象的非託管資源時,不得不這樣去做:
如果你不按照上面那種方式調用DIspose方法的話,很可能會出現錯誤(有關Dispose方法的調用,請關注新書。譯者注)。
非結構化異常處理也叫“On Error Goto”,Djikstra(艾茲赫爾·戴克斯特拉)在1974年說過“goto語句有害無益”,這已經是30年之前了!請刪除你代碼中的所有goto式的語句,我向你保證,他們萬害無一益。(艾茲赫爾·戴克斯特拉提出了“goto有害論”、訊號量和PV原語,解決了有趣的哲學家就餐問題。《軟體故事》一書中講Fortran語言時提到過他。譯者注)
總結
我希望本篇文章能夠讓一部分人能夠提高他們的編碼品質,也希望這篇文章是討論怎樣有效地進行異常處理的開始,並讓我們編寫的程式更加健壯。
譯者話:
我有一個缺點,不知道有沒有網友跟我一樣。我是個慢熱型的人,對技術也一樣,好多東西流行顛峰時期過去了我才開始有所感覺。主要一是因為我對新鮮東西不太感冒;二是我總感覺原來學習的東西還沒有掌握好就換,有點半途而廢的意思。其實我也知道這樣非常不好,畢竟IT行業是個快速發展的行業,一沒跟上步伐就落後了。
正是遇見這樣相互矛盾的情況,我在學習知識的時候都是重點學習技術間的通性,所謂通性,即十年、二十年甚至三十年不太會變、不太會沒落的東西,如果你現在從事的公司實際開發過程中一直使用某一套架構,你要是死抓著“怎樣使用這個架構做出好的系統”不放,那麼過幾年你可能就落伍了。而如果你研究研究編程中的共性,比如協議、系統間的互動原理等,這些在每個網路通訊系統中都會用到,無論是貌似已經過時了的PC程式,還是Web程式,還是當前流行的移動APP,都會用到,而且基本原理都是一樣的。看得多了,就發現新東西出來好像是換湯不換藥的感覺(稍微誇張:-))
因此,我給那些跟我一樣,不太跟隨新鮮事物的人、或者那些長期從事某一類固定開發工作的人的建議是:找准技術間的共性,不要停留在技術表面,除非你對新鮮事物足夠感興趣,並且有充分精力。
以上這些話也是我們公司開討論會時分享的。
作者:周見智
出處:http://www.php.cn/
本文著作權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文串連,否則保留追究法律責任的權利。
補充:
關於CLR的 “兩輪遍曆”異常處理策略。
當應用 程式擁有多層嵌套的異常捕獲結構時,如果最底層(其實在中介層也一樣)發生了異常,CLR將優先在引發異常的那一層去搜尋catch語句塊,看看有沒有“相容”
此類型異常的處理代碼 ,如果沒有,就“跳到”上一層去搜尋,如果上一層還沒有,繼續搜尋上一層的“上一層”,由此直到應用 程式的最頂層。
這就是CLR處理嵌套異常捕獲結構應用程式的“第一輪”遍曆-----尋找合適的例外處理常式。
如果在某一層找到了例外處理常式,注意,CLR並不會馬上執行之,而是回到"事故現場",再次進行“第二輪”遍曆,執行所有“中間”層次的finally 語句塊,然後,執行
找到例外處理常式 ,最後,再從本層開始一直遍曆到最頂層,執行所有的finally語句塊。
以上就是.NET中異常處理的最佳實務(譯)的內容,更多相關內容請關注topic.alibabacloud.com(www.php.cn)!