未檢查錯誤的代價(the cost of unchecked errors)
想像你開車時把油量警示燈的線拔掉了。看起來一切正常——儀表板沒問題、車開得好好的——直到你在高速公路上慢慢滑行停下,離加油站老遠,毫無預警。被忽略的錯誤,其危險不在你忽略它的那一刻;而在那個遠在別處、糟得多的時刻——當後果終於到來,而成因早已不見蹤影。
當你不檢查一個失敗的呼叫時,程式不會停——它會帶著一個錯誤的假設繼續跑。看看這條具體的鏈:你呼叫 write() 來存檔,它因磁碟滿了而回傳一個不足的數量或 -1,你沒檢查,於是你的程式回報「儲存成功」,使用者就關掉了應用程式。資料沒了,而好幾小時或好幾天都沒人知道。又或者:malloc() 在記憶體壓力下回傳 NULL,你沒檢查,你透過那個空指標寫入,程式就崩潰了——但崩潰發生在解參考處,那可能離真正的問題(記憶體耗盡)很遠且不相干。又或者最糟的:一個失敗的呼叫留下一個初始化到一半的緩衝區,你卻當它有效在用,於是壞資料悄悄流進其他計算、被寫進資料庫、透過網路送出、被用來做決定——默默蔓延的損壞。共通的代價是:一個未檢查的錯誤,把徵狀和成因脫鉤了:程式在離它真正出錯之處很遠的地方失敗(或更糟,錯誤地成功),而這正是讓這類臭蟲如此昂貴難追的原因。
為何重要:這就是「幹嘛要檢查每個回傳值?」的具體答案。代價不是抽象的整潔——而是資料遺失、會蔓延的損壞,以及那些位置完全說不出成因的崩潰。最陰險的情況是根本沒有崩潰的那種:未定義行為或被吞掉的錯誤能默默搞壞狀態,於是程式繼續跑、甚至看起來能動,卻產出微妙錯誤的結果。誠實的說法是:檢查錯誤是一份便宜的保險,用來抵禦昂貴、延遲、難以定位的失敗——你現在付出幾行 if 檢查,換來日後免去一場除錯惡夢(也可能免去真實世界的損害)。
FILE *f = fopen(path, "w"); fprintf(f, "%d\n", value);——如果 fopen() 失敗回傳了 NULL,fprintf() 就透過一個空的 FILE 指標寫入:在許多系統上會立刻崩潰,卻毫無線索顯示真正的成因是上面幾行那個 fopen() 呼叫的權限或磁碟問題。
崩潰落在 fprintf() 上,但真正的過錯是它上面那個沒檢查的 fopen()。
最糟的未檢查錯誤根本不會崩潰——它們默默搞壞狀態,而程式繼續產出錯誤的結果。「它跑起來沒崩潰」並不證明錯誤被處理了;一個被忽略的失敗可能一直隱形,直到它在離源頭很遠處造成最大的傷害。