版本與用語

  • 本卡依據《程式碼大全》第一版(Code Complete 1st ed., 1993,簡體中譯)第 5 章 5.6 節「防錯性編程」(p.61–67)+檢查表(p.73)+小結(p.74)寫成,每條都標書內頁碼。書裡沒寫的不寫。
  • 書中的譯法是「防錯性編程」,不是「防禦式編程」。檔名沿用舊名以免斷連結,內文以書的用語為準。
  • 引用一律照原文,只做簡→繁與明顯 OCR 錯字修正;書中的「子程序」在本卡統一寫成常式

🎯 什麼情境該想到我

當你的程式容易被異常輸入/邊界情況弄爆,想讓它更強韌時。

書的類比是防錯性駕駛

「防錯性編程並不意味著要對自己的程式提高警惕,這一想法是在防錯性駕駛的基礎上產生的,在這種駕駛方法中,必須在心中時刻認為其他駕駛員的行為都是不可預測的。這樣,就可以在他們做出某些危險舉動時,確保自己不會因此受傷。」(p.61)

「在防錯性編程中,其中心思想是,即使一個常式被傳入了壞資料,它也不會被傷害,哪怕這個資料是由其他常式錯誤而產生的。更一般地說,其思想核心是承認程式中都會產生問題、都要被改動,一個聰明的程式設計師就以這點為依據開發軟體。」(p.61)


⚠️ 先讀這一條:防錯性編程不是第一順位

書在 5.6 開頭就自己降低了這個技術的優先序:

「最有效的防錯性編碼途徑是一開始就不要引入錯誤。可以採用逐步設計方法、在編碼前先寫好 PDL、進行低層次設計、審查等都可以防止錯誤引入。**因此,應優先考慮它們,而不是防錯性編程。**不過,你可以把防錯性編程與這些技術組合起來使用。」(p.61)

所以順序是:逐步設計 → 先寫 PDL(見 工具-偽代碼編程流程)→ 低層次設計 → 審查(見 工具-軟體評審與檢查)→ 然後才是防錯性編程。


⚙️ 怎麼用(書中 5.6 的九個小節)

1. 使用斷言(5.6.1,p.61–62)

  • 斷言是「一個在假設不正確時會大聲抗議的函數或巨集指令」,用來驗證在程式中作出的假設並排除意外情況(p.61)。
  • 一個斷言函數大致有兩部分:假設為真時的布林運算式,和為假時要印出來的訊息(p.61)。書中的 Pascal 例:
    Assert(Denominator <> 0, 'Denominator is unexpectedly equal to 0.');
  • 用途分期:開發階段用來消除相互矛盾的假設、消除傳入常式的不良數值;維護階段用來表明改動是否影響到程式其他部分(p.61)。
  • 兩條指導方針(p.62):
    1. 有前置處理器就用巨集——開發階段用前置處理器處理斷言,最終程式碼要拿掉斷言時非常容易。
    2. 斷言中要避免使用可執行程式碼——關閉斷言時編譯器可能把它整段拿掉。書的反例與正解:
      ✗ Assert(FileOpen(InputFile) <> NULL, 'Couldn''t open input file');
      ✓ FileStatus := FileOpen(InputFile);
        Assert(FileStatus <> NULL, 'Couldn''t open input file');
      

舊卡上「斷言抓程式設計師的錯、驗證處理使用者的錯,兩者分開」這句已移除——第一版 5.6.1 沒有這個區分,它只講「驗證假設、排除意外情況」與開發/維護階段的用法。

2. 輸入垃圾不一定輸出垃圾(5.6.2,p.62–63)

「一個好的程式從來不會輸出亂七八糟像垃圾似的東西,不管它被輸入的是什麼。一個好程式的特點是**『輸入垃圾,什麼也不產生』,或『輸入垃圾,輸出錯誤訊息』,也可以是『不允許垃圾進入』**。從現在的觀點來看『輸入垃圾,輸出垃圾』,往往是劣質程式。」(p.62)

三件具體的事:

  • 檢查所有外部程式輸入的數值(p.62):從使用者、從檔案讀資料時,確保讀入的資料值在允許範圍之內、數值是可取的、字串短到足以處理,並在程式碼中註釋出輸入資料的允許範圍
  • 檢查全部常式輸入參數值(p.62):與檢查外部資料是同一件事,只是此時由常式代替了檔案或使用者。書給的 C 範例即在 tan() 裡先 Assert(AdjacentLength != 0, ...)
  • 決定如何處理非法參數(p.63):書列出的選項是——回傳一個錯誤代碼、回傳一個中間值、用下一個合法資料代替它並按計畫繼續執行、與上次一樣回傳一個正確答案、使用最接近的合法值、呼叫一個處理錯誤的常式、印出錯誤訊息、或乾脆關閉程式。

    「由於有這樣多的方案可供選擇,所以當在程式的任一個部分處理非法參數時,一定要仔細,確定處理非法參數的通用辦法,是由結構設計決定的,應該在結構設計層次上予以說明。」(p.63)

舊卡「回傳中性值 / 丟例外」已移除或改寫:第一版這份清單裡沒有「丟例外(throw exception)」這個選項,也沒有「中性值」這個詞(書寫的是「回傳一個中間值」)。錯誤處理策略要「全專案一致」這點書有,但書的講法是由結構設計層次規定(見 工具-建構的先決條件)。

3. 例外情況處理(5.6.3,p.63)

  • 應該預先設計好例外處理措施來注意意想不到的情況。
  • 目標是「使意外情況的出現在開發階段變得非常明顯,而在執行階段又是可以修復的」。
  • 書的例子:一個 case 只預計到五種情況——開發階段應利用例外處理產生一個警告提示出現了第六種;產品階段則做得更完善些,例如向錯誤記錄檔寫入訊息
  • 「總之,應該設計出不必費多大周折,就可以從開發階段進入產品階段的程式。」

4. 預計改動(5.6.4,p.63)

  • 改動幾乎是每個程式都不可避免的現象;即使是第一版,也會因為加入沒預計到的功能而改。
  • 「越是可能的改動,越是要容易進行。」
  • 把你在其中預想到的改動域隱含起來,是減少由於改動而對程式帶來影響的最有力武器之一。」

5. 計畫去掉除錯輔助(5.6.5,p.63–65)

除錯輔助=斷言、記憶體檢查報告、印出語句等。自用軟體留著無妨;商用軟體會影響速度與空間,所以要事先計畫怎麼拿掉。書給四種方法:

方法做法
使用版本控制開發版本含全部除錯輔助,產品階段輕鬆去掉不希望出現的部分
使用內建前置處理器#define DEBUG / #if defined(DEBUG) 圍住除錯碼;還可以給 DEBUG 賦值再測值(#if DEBUG > 0#if DEBUG == PRINTER_ERROR#if DEBUG > LEVEL_A)以區分除錯程式碼的不同層次;不喜歡滿場 #if defined() 就自訂 DebugCode(...) 巨集
自己寫一個前置處理器語言沒有前置處理器就自己寫(書說「這項工作是非常容易的」),建立標示除錯碼的約定(例:Pascal 用 /#BEGIN DEBUG//#END DEBUG/),再寫批次檔呼叫它、然後編譯處理過的程式碼——「你不會誤編譯沒有前置處理過的程式碼」
保留使用除錯常式同一個常式留兩個版本:開發版 CheckPointer() 做全面檢查;產品版 CheckPointer() 立即回傳、什麼都不做。對效能影響很小,且比自寫前置處理器快得多,兩個版本都留著供未來使用

6. 儘早引入除錯輔助工具(5.6.6,p.65–66)

「越早引入除錯輔助工具,它們所起的作用也就會越大。一般說來,只有在被某一問題困擾幾次之後,你才會捨得花功夫去編寫除錯輔助工具,但如果你在第一次遇到問題時就這樣做,或者引用一個以前遺留下的除錯輔助工具,那麼它將在整個專案中都會對你有很大幫助。」(p.65–66)

7. 使用「防火牆」包容錯誤帶來的危害(5.6.7,p.66)★

這一節就是舊卡上「Barricade」那條的真正出處,但書不叫 Barricade,叫「防火牆」。

  • 類比:建築物的防火牆把火隔離在一個地方;輪船的分隔式水密艙讓撞上冰山時只有被撞那艙進水,避免「一個洞導致全船進水」。
  • 兩個既有手段可以幫忙建防火牆:
    • 資訊隱蔽——「對另一個常式的內容知道得越少,對它如何操作的假設也就越少,而假設越少,其中一個假設出錯的可能性就會越小。」
    • 鬆散耦合——「兩個常式之間的耦合越鬆散,那麼其中一個常式中的錯誤影響到另外一個常式的機會也越少。」
  • 最好的辦法(=舊卡想講的那件事):

    「在程式中建防火牆的最好辦法是把某些介面標識成『安全』區邊界。對穿越安全區邊界的資料進行合法性檢查,如果是非法的資料,就要作出合理的反應。」(p.66)

  • 另一種說法是手術室技術

    「在資料被允許進入手術室之前,要對其進行消毒處理,手術室中的一切都認為是無毒安全的。」(p.66)
    設計時要回答的關鍵問題(原文照列):「在這個『手術室』中進入些什麼?哪些要被放在它外面?應該把門放在哪兒?應該把哪些常式放在安全區內,哪些放在外面?用什麼對資料進行消毒?」

  • 重要且常被搞反的一句

    「最簡單的辦法是在外部資料進入時對其進行消毒,但是,資料往往需要在幾個層次上進行消毒,因此,有時需要進行多重消毒。」(p.66)

8. 檢查函數回傳值(5.6.8,p.66)

  • 「如果呼叫了一個函數,並且可以忽略函數回傳值(例如,在 C 語言中,甚至不需要知道函數是否回傳一個值),千萬不要忽略這個回傳值。要對這個值進行檢查。
  • 系統函數同樣適用,除非結構設計中明訂不檢查系統呼叫的回傳碼、而是在每次呼叫後檢查錯誤代碼。發現錯誤時,C 的 perror()(或其他語言的等價物)能查出錯誤個數與說明。

9. 在最終軟體中保留多少防錯性編程(5.6.9,p.66–67)★

書點出的矛盾:開發時希望錯誤越引人注意越好;最終產品卻希望它越不顯眼越好。(p.66)五條原則:

  1. 保留查找重要錯誤的程式碼(p.66)——先確定哪些領域可以承受未測到的錯誤、哪些不能。書例:試算表程式的螢幕更新區可以承受(後果只是畫面亂);計算部分不行(會在表格中產生難以察覺的錯誤)。「絕大多數使用者都寧願忍受一個混亂的螢幕而不是錯誤的表格。」
  2. 去掉那些無關緊要錯誤的程式碼(p.66–67)——注意書的定義:「『去掉』並不是指從物理上把這段程式碼刪掉,它指的是版本控制預編譯開關,或其他不編譯那段特定程式碼的技術。」若沒有空間限制,也可以保留它並讓它隱蔽地寫入錯誤記錄檔。
  3. 去掉那些引起程式終止的程式碼(p.67)——開發階段「印出錯誤訊息然後終止」最有用;但最終軟體中「在程式終止前,使用者總是希望有機會將其工作存檔」。含有會使千百萬資料遺失的除錯碼,最終產品中應移除。
  4. 保留那些可以使程式延緩終止的程式碼(p.67)——書的親身例子:文書處理器在記憶體溢位前會亮起「SAVE」警告,讓他先存檔再退出。「我寧願得到一個警告訊息,而不願失去我前面所做的工作。」
  5. 保證留在程式中的錯誤提示訊息是友好的(p.67)——反例是他早年讓使用者在螢幕上看到「你的指標位址有錯,笨蛋!」。通常的辦法是通知使用者存在「內部錯誤」,並給一個可以投訴的電話號碼。

🧪 我實際套用的紀錄

  • 2026-07-14:(待填)

⚠️ 注意 / 什麼時候不適用

  • 不要過度防錯(5.6.9 最後一條,p.67):

    「過多的防錯性編程會帶來它自身的問題,如果你對每一種可以察覺的參數傳遞,在每一個可以察覺的地方都進行檢查,那麼程式將變得臃腫而笨拙。更糟的是,附加的用於防錯性編程的程式碼本身並非是完善無缺的,同其他程式碼一樣,你也會在其中發現錯誤,而且,如果你是隨意寫它的,那麼錯誤也會更多。考慮好需要在哪裡預防錯誤,然後再使用防錯性編程。

  • ⛔ 舊卡「在邊界驗證一次,內部就能假設乾淨」這條與第一版原文相牴觸,已移除。 書明說「資料往往需要在幾個層次上進行消毒,因此,有時需要進行多重消毒」(p.66)。真正的節制原則是上面那條「考慮好需要在哪裡預防錯誤」,而不是「只驗一次」。
  • 防錯性編程排在預防手段之後(p.61),別拿它當作省略設計與審查的藉口。
  • 它的功用是縮小損害、不是消滅錯誤(小結,p.74):

    「防錯性編程可以使錯誤更容易被發現和修復,對最終軟體的危害性顯著減小。」

隨手用的檢查表(p.73,5.8.2 檢查表「防錯性編程」段,原文照列)

  • 斷言是否用於驗證假設?
  • 常式對於非法輸入資料進行防護了嗎?
  • 常式是否能很好地進行程式終止?
  • 常式是否能很好地處理修改情況?
  • 是否不用很麻煩地啟用或去掉除錯輔助?
  • 是否資訊隱蔽、鬆散耦合,以及使用「防火牆」資料檢查,以使得它不影響常式之外的程式碼?
  • 常式是否檢查回傳值?
  • 產品程式碼中的防錯性程式碼是否幫助使用者,而不是程式設計師?

🔗 相關工具

  • 工具-建構的先決條件(同書)——書把兩件事推到結構設計層次:處理非法參數的通用辦法「是由結構設計決定的,應該在結構設計層次上予以說明」(p.63),以及結構設計要規定斷言的使用程度與錯誤處理策略。先決條件沒過就談防錯,順序是反的。
  • 工具-偽代碼編程流程(同書)——5.6 開頭列的預防手段之一就是「在編碼前先寫好 PDL」(p.61);反過來,第 4 章收尾檢查也要求確認常式「採用了預防錯誤的設計」(p.41)。兩張卡是同一條防線的上下游。
  • 工具-軟體評審與檢查(同書)——同樣出自 p.61 那句「審查……應優先考慮它們,而不是防錯性編程」;而第 5 章檢查表(p.73)本身就是一份可以拿去做正式檢查的清單。
  • 工具-用例外處理錯誤程式碼整潔之道)——對照用,不是同一套:第一版《程式碼大全》處理非法參數的選項清單裡沒有「丟例外」(p.63),它給的是錯誤碼/中間值/最接近的合法值/呼叫錯誤處理常式/終止等。要拿例外當策略時,請知道那是另一本書的主張。
  • 工具-MQ消費端防禦三原則——實務對應:訊息佇列消費端就是典型的「安全區邊界」,在邊界做合法性檢查與合理反應(p.66)。
  • 回到來源:程式碼大全(第一版,第 5 章 5.6 節「防錯性編程」,p.61–67)