消除例外處理複雜性的最佳做法:定義 API 時讓錯誤根本不存在

聽起來像褻瀆,但實務上極其有效。

Tcl unset 的修正版本#

回到 Tcl unset 的例子:

  • 原本unset 拋例外當變數不存在
  • 修正後unset 不再拋例外,遇到不存在的變數直接什麼都不做就回傳

語意上的微調是關鍵:

舊定義新定義
刪除一個變數」確保某變數不存在」
變數不存在 → 工作做不了 → 必須報錯變數不存在 → 工作已經完成 → 直接回傳

透過調整定義的「義務」描述,把錯誤條件直接消失——不再有任何「錯誤情境」需要回報。

範例:Windows 的檔案刪除#

Windows 的設計:

  • 檔案被某行程開著時,禁止刪除
  • 使用者必須找到佔用檔案的行程並 kill 它
  • 有時甚至重開機才能刪檔

Unix 的設計優雅得多:

  • 檔案被開著時呼叫 delete → 不立即刪除,而是標記為待刪
  • 檔名從目錄移除(其他行程無法再開到舊檔;同名新檔可建)
  • 已開啟該檔的行程繼續正常讀寫
  • 全部關閉後資料才釋放

Unix 一次消除了兩種錯誤

  • 檔案使用中時 delete 不再失敗
  • 不會對正在使用該檔案的行程引發新例外

替代設計(立即刪除並讓所有開啟失效)會製造新錯誤讓行程處理。延遲刪除把錯誤從定義消除。

「Unix 允許行程繼續讀寫一個註定要被刪除的檔案」聽起來很奇怪,但作者從未見過這引發顯著問題。

範例:Java substring#

Java 的 String.substring(begin, end)

  • 只要任一索引超出字串範圍 → IndexOutOfBoundsException

問題:當你想**抓取「字串中與指定範圍重疊的部分」**時——

  • 一兩個索引可能超出範圍
  • 因為原 API 會拋例外,呼叫端必須自己 clamp 索引
  • 一行 API 變成 5 ~ 10 行程式碼

改善版 API#

「回傳所有索引大於等於 beginIndex 且小於 endIndex 的字元(如果有)。」

  • 簡潔、自然
  • 無論索引是否負數、beginIndex > endIndex行為都已定義良好
  • API 同時變簡單與更強——方法變得更深

Python 已是這個模式:list slicing 超出範圍只回空。

「讓錯誤消失會不會反而隱藏 bug?」#

有人擔心:拋例外能抓 bug,消除錯誤不會讓 bug 更難發現嗎?

多錯誤的 API 確實可能抓到一些 bug,但也增加複雜度——而複雜度本身會引發新的 bug。

  • 開發者得寫額外程式碼避開 / 忽略錯誤
  • 忘記寫額外程式碼 → runtime 拋出意外例外

「把錯誤消除」反而讓 API 簡單、需要寫的程式碼變少。

減少 bug 的最佳辦法是讓軟體更簡單。