15防護式編程1

一. 防護式編程概述

​ 防護式編程,這一律念來自防護式駕駛。在防護式駕駛中要創建這樣一種思惟,那就是你永遠也不能肯定另外一位司機將要作什麼。這樣才能確保在其餘人作出危險動做時你也不會受到傷害。你要承擔起保護本身的責任,那怕是其餘司機犯的錯誤。java

​ 防護式編程的主要思想是:子程序應該不因傳入錯誤數據而被破壞。哪怕是由其餘子程序產生的錯誤數據。更通常的說,其核心想法是要認可程序都會有問題,都須要被修改,聰明的程序員應該根據這一點來編程。程序員

​ 本章就是要講述如何面度嚴酷的非法數據的世界、在遇到「毫不會發生」的事件以及其餘程序員犯下的錯誤時保護你本身。面試

​ 防護式編程是本書所介紹的其餘提升軟件質量技術的有益輔助手段。防護式編程的最佳方式就是在一開始不要在代碼中引入錯誤。使用迭代式設計、編碼前先寫僞代碼、寫代碼前先寫測試用例、低層設計檢查等活動,都有助於防止引入錯誤。所以,要在防護式編程以前優先運用這些技術。編程

二. 保護程序免遭非法輸入數據的破壞

​ 一般有三種方法來處理進來垃圾的狀況。數組

1. 檢查全部來源於外部的數據的值

​ 當從文件、用戶、網絡或其餘外部接口中獲取數據時,應檢查所得到的數據值,以確保它在容許的範圍內。對於數值,要要確保它在可接受的取值範圍內;對於字符串,要確保其不超長。若是字符串表明的是某個特定範圍內的數據(如金融交易ID或其餘相似數據),那麼要確認其取值呵護用途,不然就應該拒絕接受。若是你在開發須要確保安全的應用程序,還有格外注意哪些狡猾的多是攻擊你的系統的數據,包括企圖令緩存區溢出的數據、注入的SQL命令、注入的HTNL或XML代碼、整數溢出以及傳遞給系統調用的數據,等等。緩存

2. 檢查子程序全部輸入參數的值

​ 檢查子程序輸入參數的值,事實上和檢查來源於外部的數值同樣,只不過數據是來自於其餘子程序而非外部接口。下面的章節「隔離程序,使之包容由錯誤形成的損害」闡述了一種實用方法可用於肯定哪些子程序須要檢查其輸入數據。安全

3. 決定如何處理錯誤的輸入數據

​ 一旦檢測到非法的參數,你該如何處理它呢?根據狀況不一樣,你能夠從十幾種不一樣的方案中選擇其一,後面的章節「錯誤處理技術」中會詳細描述這些技術。網絡

三. 斷言

1. 斷言概述

​ 斷言(assertion)是指在開發期間使用的、讓程序在運行時進行自檢的代碼(一般是一個子程序或宏)。斷言爲真,則代表程序運行正常,而斷言爲假,則則意味着它已經在代碼中發現了意料以外的錯誤。架構

​ 斷言對於大型的複雜程序或可靠性要求極高的程序來講尤爲有用。經過使用斷言,程序員能更快速地排查出因修改代碼或者別的緣由,而弄進程序裏的不匹配的接口假定和錯誤等。編程語言

​ 一個斷言一般含有兩個參數:一個描述假設爲真時的狀況的布爾表達式,和一個斷言爲假時須要顯示的信息。下面試假定變量 denominator(分母)的值應該爲非零值時Java斷言的寫法。

assert denominator != 0 : "denominator si unexpectedly equal to 0.";

​ 這個斷言聲明 denominator 不會等於 0 。 其中第一個參數,denominator != 0, 是個布爾表達式,其結果爲true或false。第二個參數是當第一個參數爲fasle時——即斷言爲假時——要打印的消息。

​ 斷言能夠用於在代碼中說明各類假定,澄清各類不但願的情形。能夠用斷言檢查以下這類假定:

  • 輸入參數或輸出參數的取值處於預期的範圍內。

  • 子程序開始(或者結束)執行時文件或流是處於打開(或關閉)的狀態。

  • 子程序開始(或結束)執行時,文件或流的讀寫位置處於開頭(或結尾)處。

  • 文件或流已用只讀、只寫或可讀可寫方式打開。

  • 僅用於輸入的變量的值沒有被子程序所修改;

  • 指針非空

  • 傳入子程序的數組或其餘 容器至少能容納 X 個數據元素。

  • 表已初始化,存儲着真實的數值。

  • 子程序開始(或結束)執行時,某個容器是空的(或滿的)。

  • 一個通過高度優化的複雜子程序的運算結果和相對緩慢但代碼清晰地子程序的運算結果相一致。

固然,這裏列出的知識一些基本假定,你在子程序中還能夠包含更多能夠用斷言來講明的假定。

正常狀況下,你並不但願用戶看到產品代碼中的斷言信息;斷言主要是用於開發和維護階段。一般,斷言只是在開發階段被編譯到目標代碼中,而在生成產品代碼時並不編譯進去。在開發階段,斷言能夠幫助查清相互矛盾的假定、預料以外的狀況以及傳給子程序的錯誤數據等。在生成產品代碼中,能夠不把斷言編譯進目標代碼裏去,以避免下降系統的性能。

##### 2. 創建本身的斷言機制

​ 包括 C++、Java等在內的不少語言都支持斷言。若是你用的語言不支持斷言語句,本身寫也是很容易的。C++ 中標準的 assert 宏並不支持文本信息。下面的例子給出了一個使用 C++ 宏改進的 ASSERT 實現:

#define ASSERT( condition, message){
  if(!(condition))
    {
        LogError("Assertion failed: ", #conditionm message);
        exit(EXIT_FAILURE);
    }
}

#### 3. 使用斷言的指導意見

###### 3.1 用錯誤處理代碼來處理預期會發生的情況,用斷言來處理覺不該該發生的情況

​ 斷言是用來檢查永遠不應發生的狀況,而錯誤處理代碼是用來檢查不太可能常常發生的非正常狀況,這些狀況是能在寫代碼時就預料到的,且在產品代碼中也要處理這些狀況。錯誤處理一般用來檢查有害的輸入數據,而斷言是用於檢查代碼中的bug。

​ 用錯誤處理代碼來處理反常狀況,程序就可以很從容地對錯誤作出反應。若是在發生異常狀況的時候觸發了斷言,那麼要採起的更正的措施就不只僅是對錯誤作出恰當的反映了——而是應該修改程序的源代碼並從新編譯,而後發佈軟件的新版本。

​ 有種方式可讓你更好地理解斷言,那就是把斷言看作是可執行的註解——你不能依賴它來讓代碼正常工做,但與編程語言中的註釋相比,他能更主動地對程序中的假定作出說明。

###### 3.2. 避免把須要執行的代碼放到斷言中

若是把代碼寫在斷言裏,那麼當你關閉斷言功能時,編譯器極可能就把這些代碼排除在外了。應該把須要執行的語句提取出來,並把其運行結果賦給狀態變量,再對這些狀態變量進行判斷。下面這樣使用斷言就更安全:
actionPerformed = PerformAction()
Debug.Assert(actionPerformed) 'Couldn't perform action

###### 3.3. 用斷言來註解並驗證前條件和後條件

​ 前條件和後條件是一種名爲「契約式設計」的程序設計和開發方法的一部分。使用前條件和後條件時,每一個子程序或類與程序的其他部分都造成了一份契約。

​ 前條件是子程序或類的調用方代碼在調用子程序或實例化對象以前要確保爲真的屬性。前條件時調用方代碼對其所調用的代碼要承擔的義務。

​ 後條件時子程序或類在執行結束後要確保爲真的屬性。後置條件時子程序或類對調用方代碼所承擔的責任。

​ 斷言是用來講明前置條件和後置條件的有力工具。也能夠用註釋來講明前條件和後條件,但斷言卻能動態地判斷前條件和後條件是否爲真。

###### 3.4. 對於高健壯性的代碼,應該先使用斷言再處理錯誤

​ 對於每種可能出錯的條件,一般子程序要麼使用斷言,要麼使用錯誤處理代碼來進行處理,可是不會同時使用兩者。

四. 錯誤處理技術

1 概述

​ 斷言能夠用於處理代碼中不該該發生的錯誤。那麼又該如何處理那些預料之中可能發生的錯誤呢?根據所處情形的不一樣,你能夠返回中立值、換用下一個正確數據、返回與前次相同的值、換用最接近的有效值、在日誌文件中記錄警告信息、返回一個錯誤碼、調用錯誤處理子程序或對象、顯示出錯信息或者關閉程序——或把這些技術結合起來使用。

1.1 返回中立值
1.2 換用下一個正確的數據
1.3 返回與前次相同的數據
1.4 換用最接近的合法值
1.5 把警告信息記錄到日誌中
1.6 返回一個錯誤碼
1.7 調用錯誤處理子程序或對象
1.8 當錯誤發生時顯示出錯誤
1.9 用最穩當的方式在局部處理錯誤
1.10 關閉程序

​ 正如前面視頻遊戲和 X 光機的例子告訴咱們的,處理錯誤最恰當的方式要根據出現錯誤的軟件的類別而定。這兩個例子還代表,錯誤處理的方式有時更側重於正確性,而有時更側重於健壯性。開發人員傾向於非形式地使用這兩個術語,但嚴格來講,這兩個術語在程度上是截然相反的。正確性意味着永不返回不許確的結果。哪怕不返回結果也比返回不許確的結果好。然而,健壯性則意味着要不斷嘗試採起某些措施,以保證軟件能夠持續地運轉下去,哪怕有時作出一些不夠準確的結果。

​ 人生安全攸關的軟件每每更傾向於正確性而非健壯性。不返回結果也比返回錯誤的結果要好。放射線治療儀就是體現這一原則的好例子。

​ 消費類應用軟件每每更注重健壯性而非正確性。一般只要返回一些結果就比軟件中止運行要強。我所用的字處理軟件有時會在屏幕下方顯示半行文字。若是它檢測到這一狀況,難道我指望字處理軟件退出嗎?固然不。

2. 高層次設計對錯誤處理方式的影響

​ 既然有這麼多的選擇,你就必須注意,應該在整個程序裏採用一致地方式處理非法的參數。對錯誤進行處理的方式會直接關係到軟件可否知足在正確性、健壯性和其餘非功能性指標方面的要求。肯定一種通用的處理錯誤參數的方法,是架構層次(或稱高層次)的設計決策,須要在那裏的某個層次上解決。

​ 一旦肯定了某種方法,就要確保始終如一地貫徹這一方法。若是你決定讓高層次的代碼處理錯誤,而低層次的代碼只需簡單地報告錯誤,那麼就要確保高層次的代碼真的處理了錯誤!有些語言容許你忽略「函數返回的是錯誤碼」這一事實——在C++中,你無須對函數的返回值作任何處理——但千萬不要忽略錯誤信息!檢查函數的返回值。即便你認定某個函數不會出錯,也不管如何要去檢查一下。防護式編程所有的重點就在於防護那些你不曾預料到的錯誤。

​ 這些指導建議對於系統函數和你本身寫的函數都是成立的。除非你已確立了一套不對系統調用進行錯誤檢查的架構性指導建議,不然請在每一個系統調用後檢查錯誤碼。一旦檢測到錯誤,就記下錯誤代號和它的描述信息。

五. 異常

1. 概述

​ 異常是把代碼中的錯誤或異常事件傳遞給調用方代碼的一種特殊手段。若是在一個子程序中遇到了預料以外的狀況,但不知道該如何處理的haul,它就能夠跑出一個異常,就比如是舉起雙說「我不知道該怎麼處理它——我真但願有誰知道該怎麼辦!」同樣。對出錯的來龍去脈不甚瞭解的代碼,能夠把控制權轉交給系統中其餘能更好地解釋錯誤並採起措施的部分。

​ 異常和繼承有一點是相同的,即:審慎明智地使用時,它們均可以下降複雜度;而草率粗心地使用時,只會讓代碼變得幾乎沒法理解。下面給出的一些建議可讓你在使用異常時揚長避短,並避免一直相關的一些難題。

1.1 用異常通知程序的其餘部分,發生了不可葫蘆哦的錯誤

​ 異常機制的優越之處在於它能提供一種沒法被忽略的錯誤通知機制。其餘的錯誤處理機制有可能會致使錯誤在不知不覺中向外擴散,而異常則消除了這種可能性。

1.2 只在真正例外的狀況下才拋出異常

​ 僅在真正例外的狀況下才使用異常——換句話說,就是僅在其餘編程實踐方法沒法解決的狀況下才使用異常。異常的應用情形和斷言類似——都是用來處理那些不只罕見甚至永遠不應發生的狀況。

​ 異常須要你作出一個取捨:一方面他是一種強大的用來處理預料以外的狀況的途徑,另外一方面程序的複雜度會所以增長。因爲調用子程序的diamante須要瞭解別調用代碼中可能會拋出的異常,所以異常弱化了封裝性。同時,代碼的複雜度也會有所增長。

1.3 不能用異常來推卸責任

​ 若是某種的錯誤狀況能夠在局部處理,那就應該在局部處理掉它。不要把原本能夠在局部處理的錯誤當成一個未被捕獲的異常拋出去。

1.4 避免在構造函數和析構函數中拋出異常,除非你在同一個地方把他們捕獲

​ 當從構造函數和析構函數裏拋出異常時,處理異常的規則立刻就會變得很是負責。好比說在C++裏,只有在對象已徹底構造以後纔可能調用析構函數,也就是說,若是在構造函數的代碼中拋出異常,就不會調用析構函數,從而形成潛在的資源泄漏。在析構函數中拋出異常也有相似複雜的規則。

1.5 在恰當的抽象層次拋出異常
1.6 在異常消息中加入關於致使異常發生的所有信息

​ 每個異常都是發生在代碼拋出異常時所遇到的特殊狀況下。這一信息對於讀取異常消息的人們來講是頗有價值的,所以要確保該消息中含有爲理解異常拋出緣由所須要的信息。若是異常時由於一個數組下標錯誤而拋出的,就應該在異常消息中包含數組的上界、下界以及非法的下標值等信息。

1.7 避免使用空的 catch 語句
1.8 瞭解所用函數庫可能拋出的異常
1.9 考慮建立一個集中的異常報告機制
1.10 把項目中對異常的使用標準化

​ 若是你在使用一種像C++同樣的語言,其中容許拋出多種多樣的對象、數據及指針的話,那麼久應該爲到底能夠拋出哪些種類的異常創建一個標準。爲了與其餘語言相兼容,能夠考慮只拋出從 std::exception 積累派生出的對象。

  • 考慮建立項目的特定異常類,它可用做項目中全部可能拋出的異常的基類。這樣就能把記錄日誌、報告錯誤等操做集中起來並標準化。
  • 規定在何種場合容許代碼使用 throw-catch 語句在局部對錯誤進行處理。
  • 規定在何種場合容許代碼拋出不在局部進行處理的異常。
  • 肯定是否要使用集中的異常報告機制。
  • 規定是否容許在構造函數和析構函數中使用異常。
1.11 考慮異常的替換方案

​ 有些程序員用異常來處理錯誤,只是由於他所用的編程語言提供了這種特殊的錯誤處理機制。你內心應該至始至終考慮各類各樣的錯誤處理機制:在局部處理錯誤、使用錯誤碼來傳遞錯誤、在日誌文件中記錄調試信息、關閉系統或其餘的一些方式等。僅僅由於編程語言提供了異常處理機制而使用異常,是典型的「爲用而用」;這也是典型的「在一種語言上編程」而非「深刻一種語言去編程」的例子。

​ 最後,請考慮你的程序是否真的須要處理異常。就像 Bjarne Stroustruop所指出的,應對程序運行時發生的嚴重錯誤的最佳作法,有時就是釋放全部已得到的資源並終止程序執行,而讓用戶去從新用正確的輸入數據再次運行程序。

六. 隔離程序

1. 隔離程序,使之包容由錯誤形成的損失

​ 以防護式編程爲目的而進行隔離的一種方法,是把某些接口選定爲「安全」區域的邊界。對穿越安全區域邊界的數據進行合法性校驗,並當數據非法時作出敏銳的反應。

image

​ 也能夠一樣在類的層次採用這種方法。類的公用方法能夠假設數據是不安全的,它們要負責檢查數據並進行清理。一旦類的公用方法接收了數據,那麼類的私用方法就能夠假定數據都是安全的了。

​ 也能夠把這種方法看作是手術室裏使用的一種技術。任何東西在容許進入手術室以前都要通過消毒處理。所以手術室內內的任何東西均可以認爲是安全的。這其中最核心的設計決策就是規定什麼能夠進入手術室,什麼不能夠進入,還有把手術室的門設在哪裏——在編程中也就是規定,哪些子程序可認爲是在安全區域裏的,哪些又是在安全區域外的,哪些負責清理數據。完成這一工做最簡單的方法是在獲得外部數據時當即進行清理,不過數據每每須要通過一層以上的清理,所以多層清理有時也是必要的。

2. 在輸入數據時將其轉換爲恰當的類型

​ 輸入的數據一般都是字符串或數字的形式。這些數據有時要被映射爲」是「或」否「這樣的布爾類型,有時要被映射爲像 Color_Red、Color_Green 和 Color_Blue 這樣的枚舉類型。在程序中長時間傳遞類型不明的數據,會增長程序的複雜度和崩潰的可能性——好比說有人在須要輸入顏色枚舉值的地方輸入了」是「。所以,應該在輸入數據後當即將其轉換到恰當的類型。

3. 隔離與斷言的關係

​ 隔離的使用使斷言和錯誤處理有了清晰地區分。隔欄外部的程序應使用錯誤處理技術,在那裏對數據作的任何假定都是不安全的。而隔欄內部的程序裏應使用斷言技術,由於傳進來的數據應該已在經過隔欄時被清理過了。若是隔欄內部的某個子程序檢測到錯誤的數據,那麼這應該是程序裏的錯誤而不是數據裏的錯誤。

​ 隔欄的使用還展現了「在架構層次上規定應該如何處理錯誤」的價值。規定隔欄內外的代碼是一個架構層次上的決策。

相關文章
相關標籤/搜索