除錯(Debugging)是找出錯誤根源並加以修正的過程,與偵測錯誤的「測試」互為互補。在某些專案中,除錯佔據了總開發時間的 50%。然而,研究顯示最優秀與最差的程式設計師在除錯效率上有高達 20:1 的差距——好消息是,透過系統化的方法,除錯不必是最困難的環節。

23.1 除錯概述#

除錯和測試一樣,本身並不是提升品質的手段,而是診斷缺陷的方式。真正的品質必須從需求、設計與高品質的編碼實踐中建立,除錯只是最後一道防線。

經典研究(Gould 1975)發現,最快與最慢的程式設計師在除錯效能上有巨大差異:最快的三人平均 5 分鐘找到缺陷、引入 3 個新缺陷;最慢的三人平均 14.1 分鐘、引入 7.7 個。若遞迴計算修正引入的新缺陷,最慢群組需要約 13 倍時間才能完全除錯——印證了提升品質同時降低開發成本的原則。

缺陷即學習機會#

每個缺陷都映照出你對程式的理解不足。善用機會去:理解程式本身、認識自己的錯誤模式、從讀者角度審視程式碼品質、改進除錯方法。

無效的除錯方式#

方式說明
靠猜測找缺陷隨機散佈 print 語句,東改西改直到「好像」能動
不花時間理解問題認為問題很小,找到就好
修表面不修根源用特殊判斷硬改結果(如 if (client == 45) sum[45] += 3.45
迷信式除錯怪機器、怪編譯器——程式有問題,就是你的錯

即使錯誤看似不是你的問題,先假設它是你的責任。這個心態能幫助你更快找到缺陷,也能維護你的專業信譽。

23.2 尋找缺陷#

找到並理解缺陷通常佔除錯工作的 90%。有效的程式設計師不靠隨機猜測,而是運用科學方法(Scientific Method)系統性地除錯。

科學方法的除錯步驟#

  1. 穩定錯誤(Stabilize the error)——使錯誤可重現
  2. 定位來源(Locate the source)
    • 蒐集產生缺陷的資料
    • 分析資料,形成假設
    • 設計方式來驗證或推翻假設
    • 執行驗證
  3. 修正缺陷(Fix the defect)
  4. 測試修正(Test the fix)
  5. 尋找類似錯誤(Look for similar errors)
flowchart TD
    A["穩定錯誤"] --> B["蒐集資料"]
    B --> C["分析"]
    C --> D["形成假設"]
    D --> E["設計驗證"]
    E --> F["執行驗證"]
    F --> G{"假設成立?"}
    G -->|"是"| H["修正缺陷"]
    H --> I["測試修正"]
    I --> J["尋找類似錯誤"]
    G -->|"否"| B

穩定錯誤#

無法穩定重現的錯誤幾乎不可能診斷。間歇性錯誤通常源自:

  • 初始化問題:變數未正確初始化,偶爾剛好為零
  • 時序問題(Timing issue)
  • 懸空指標(Dangling pointer)

穩定化的目標不只是找到一個重現的測試案例,而是將它簡化到最小——任何面向的改變都會影響錯誤的行為。

尋找缺陷的實用技巧#

點擊展開:尋找缺陷的技巧清單
  • 使用所有可用資料形成假設:假設必須能解釋所有已觀察到的現象,否則就繼續精煉
  • 精煉測試案例:逐步調整條件,觀察哪些變化會影響錯誤
  • 在單元測試中執行相關程式碼:將可疑程式碼抽出來獨立測試
  • 善用工具:互動式除錯器、記憶體檢查器、語法導向編輯器等
  • 用多種方式重現錯誤:從不同角度三角定位缺陷的真正位置
  • 產生更多資料以產生更多假設:選擇與已知案例不同的測試輸入
  • 善用否定測試的結果:被推翻的假設也能縮小搜索範圍
  • 腦力激盪:不要只鑽研第一個假設,多想幾個可能性再逐一排除
  • 寫下待嘗試的事項:避免在同一條死路上卡太久
  • 縮小可疑範圍:用二分搜尋法,移除一半程式碼看錯誤是否仍在
  • 注意曾出錯的類別與常式:缺陷傾向群聚
  • 檢查最近變更的程式碼:新錯誤通常與近期修改有關
  • 擴大可疑範圍:若在小區域找不到,考慮缺陷可能在別處
  • 增量整合:一次加入一小塊程式碼,新錯誤必定來自新加入的部分
  • 對照常見缺陷清單:利用檢查表刺激思考
  • 跟別人說:解釋問題的過程中,常常自己就找到答案(「告解式除錯」)
  • 休息一下:若焦慮感出現,就是該休息的信號——讓潛意識接手

暴力除錯法#

當精密分析行不通時,暴力法(Brute-force)雖然費力但保證有效

  • 對問題程式碼做完整的設計與程式碼審查
  • 丟掉問題段落,從頭重寫
  • 用最嚴格的編譯器警告等級編譯並修正所有警告
  • 掛上單元測試框架,獨立測試問題程式碼
  • 在除錯器中手動逐步執行大型迴圈
  • 用不同的編譯器或環境編譯與執行

設定一個「快速除錯」的時間上限。若五分鐘的猜測法沒用,就切換到保證有效的暴力法,避免浪費數小時在碰運氣上。

語法錯誤#

語法錯誤隨著現代編譯器的進步已越來越少見,但仍需注意:

原則說明
不要相信行號編譯器可能把錯誤報在錯誤的位置
不要完全相信錯誤訊息編譯器的描述可能有誤導性
不要信第二個錯誤訊息修正第一個再重新編譯即可
分而治之移除部分程式碼重新編譯,定位語法錯誤的位置

23.3 修正缺陷#

找到缺陷是困難的部分,但修正才是容易出錯的部分——研究發現缺陷修正首次正確的機率不到 50%(Yourdon 1986b)。

  • 先理解問題再修正:用能重現和不能重現錯誤的案例來三角驗證你的理解
  • 理解程式,不只是問題:對程式有全局理解的開發者修正成功率更高——至少理解缺陷周圍數百行程式碼
  • 確認診斷:排除所有其他可能的原因後再動手
  • 放輕鬆:匆忙修正會導致草率判斷與不完整的診斷
  • 保存原始原始碼:修正前先備份,方便日後比對
  • 修根源,不修表象:不要用特殊案例的 if 判斷來掩蓋問題
  • 只在有充分理由時才改程式碼:修改後若結果出乎意料,代表你還不夠理解問題
  • 一次只改一處:避免多個修改交互作用產生新的混淆
  • 檢查你的修正:用自動化迴歸測試確認修正沒有副作用
  • 新增暴露此缺陷的單元測試:避免缺陷將來被重新引入
  • 尋找類似的缺陷:缺陷傾向群聚,若想不到如何搜尋類似缺陷,代表你還不夠理解問題

絕不要隨機修改程式碼直到「好像能動」。這是巫毒程式設計(Voodoo Programming)——你沒有學到任何東西,對正確性也毫無信心。

23.4 除錯中的心理因素#

除錯要求你在流暢的創造性思維與嚴格的批判性思維之間快速切換,同時還要對抗自己「我的程式碼沒問題」的自我保護心態。

心理定勢(Psychological Set)#

心理定勢是指人們傾向看到自己預期看到的東西:

  • 學生期待 while 迴圈像自然語言中的「當…就…」一樣持續評估,而非只在迴圈頂端或底端檢查
  • 程式設計師把 SYSTSTSSYSSTSTS 當作同一個變數,直到程式跑了數百次才發現
  • 看到沒有大括號的 if 後面跟著多行縮排,自動腦補大括號的存在

良好的程式風格(格式化、命名、註解)能建立一致的視覺背景,讓缺陷作為「變異」更容易被察覺。

心理距離(Psychological Distance)#

心理距離指兩個項目被區分的容易程度。變數名稱如 stoppt vs. stcppt 幾乎不可見,而 product vs. sum 則一目了然。選擇命名時應確保足夠的心理距離,避免因視覺相似而引發除錯盲區。

23.5 除錯工具#

原始碼比對工具#

Diff 等比對工具(Source-code Comparator)可用來比較修改前後的版本,迅速定位差異。當新版本出現舊版沒有的缺陷時,比對檔案是最直接的方法。

編譯器警告訊息#

  • 將警告等級設到最嚴格,並修正所有警告
  • 將警告視為錯誤(Treat warnings as errors)
  • 建立整個專案統一的編譯器設定標準

其他工具#

工具說明
延伸語法與邏輯檢查器(如 lint)比編譯器更嚴格地檢查未初始化變數等問題
執行效能分析器(Execution Profiler)花幾分鐘研究程式效能分佈,可能揭露隱藏的缺陷
測試框架與鷹架程式碼(Test Framework / Scaffolding)將問題程式碼抽出獨立測試
互動式除錯器(Debugger)設定中斷點、逐步執行、檢查資料、倒退執行等

除錯器不能取代思考,但思考有時也不能取代好的除錯器。最有效的組合是好的思考加上好的除錯器

更多資源#

  • Agans, David J. Debugging: The Nine Indispensable Rules for Finding Even the Most Elusive Software and Hardware Problems. Amacom, 2003.
  • Myers, Glenford J. The Art of Software Testing. John Wiley & Sons, 1979. 第七章專門討論除錯。
  • Allen, Eric. Bug Patterns In Java. Apress, 2002. 提出與本章類似的科學方法論除錯框架。

要點#

  • 除錯是軟體開發中影響甚鉅的環節。最佳策略是用本書其他技術從源頭避免缺陷,但提升除錯技能仍然值得,因為優劣之間的差距至少 10:1
  • 系統化方法是成功的關鍵。每次測試都應使你往前一步,善用科學方法來除錯。
  • 在修正程式之前,先徹底理解根本問題。隨機猜測與隨機修改只會讓程式變得更糟。
  • 將編譯器警告設到最嚴格的等級,並修正所有報告的問題——連明顯的錯誤都不修,遑論找到隱微的缺陷。
  • 除錯工具是強大的輔助,找到它們、使用它們,同時記得運用你的大腦。