Tony Hoare,null
的發明者,稱它為「十億美元的錯誤」。null
和 undefined
是程式設計史上最多產的 bug 來源。
一個回傳 null
或 undefined
的函式,就像一個隨手把地雷埋在程式碼裡然後走開的工程師。
他把排雷的責任,丟給了每一個未來會與這段程式碼互動的人。
用空陣列、明確的結果物件或拋出錯誤,別丟個
null
讓人猜。
null
假裝自己是一個正常的值,但當你試圖對它做任何事情時——比如 user.name
——它就會立刻爆炸,給你一個 TypeError: Cannot read properties of null
。
// 🔴 臭味:回傳 null 或 undefined,就像不負責任的渣男
function findUser(id) {
// Array.prototype.find 在找不到時,原生回傳的是 undefined
const user = database.users.find(u => u.id === id);
// 開發者有時會「畫蛇添足」地把它轉成 null,兩者一樣危險
return user || null;
}
// 現在,每一個呼叫者都「必須」記住要檢查這個地雷。
const user = findUser(123);
// 這種防禦性檢查會像瘟疫一樣,在你的程式碼庫裡到處都是。
if (user) { // 同時檢查 null 和 undefined
console.log(user.name);
if (user.profile) {
// ... 沒完沒了
}
}
不負責任: 「找不到」是一種可能情況,但卻選擇把處理這個問題的負擔,轉嫁給了成百上千個呼叫者。
依賴人類脆弱的記憶: 你必須記得去檢查空值>
它的型別不一致: 這個函式的回傳型別是什麼?是 User
或者 null
或者 undefined
。這讓靜態分析和開發者的心智負擔都大大增加。
專業的程式設計師,會設計一個預設安全 (safe by default) 的 API。
回傳一個結構,讓呼叫者即使不加檢查,程式碼也不會爆炸。
如果你的函式回傳一個列表(陣列),在找不到任何東西時,永遠回傳一個空陣列 []
。
// 🟢 好味道:絕對安全,符合直覺。
function findUsersByRole(role) {
const users = database.users.filter(u => u.role === role);
return users; // 如果 filter 結果是空的,它自然就是 []
}
// 呼叫者完全不需要寫任何 if (results !== null) 的檢查。
const admins = findUsersByRole('admin');
// 這段程式碼在 admins 是空陣列時,會優雅地什麼都不做,而不是爆炸。
for (const admin of admins) {
console.log(admin.name);
}
console.log(`Found ${admins.length} admins.`); // 會印出 "Found 0 admins."
這個模式是處理集合問題的黃金準則。
如果函式是查找單一物件,null
是最危險的回應。
你應該回傳一個「信封」,清楚告知「操作是否成功」以及「結果是什麼」。
// 🟢 好味道:契約清晰,強迫呼叫者處理失敗情況。
function findUser(id) {
const user = database.users.find(u => u.id === id);
if (!user) {
// 信封裡寫著「失敗」,並附上原因。
return { ok: false, error: 'user_not_found' };
}
// 信封裡寫著「成功」,並附上資料。
return { ok: true, data: user };
}
// 呼叫者被迫打開信封,檢查狀態。
const result = findUser(123);
if (result.ok) {
// 只有在確定 ok 的情況下,才去使用 data。絕對安全。
console.log(result.data.name);
} else {
console.error(`Failed to find user: ${result.error}`);
}
這個模式把一個容易被遺忘的執行時錯誤(null
爆炸),轉變成了一個必須處理的邏輯分支。
Result 模式雖然強大,但並非是萬能解法。
我們需要區分兩種失敗:
可預期的失敗 (Failure):這是業務邏輯的一部分,是「正常」的失敗路徑。例如「使用者不存在」、「密碼錯誤」。使用 Result 物件是完美的選擇。
意外的錯誤 (Error/Exception):這是系統層級的、非預期的異常。例如「資料庫連線中斷」、「設定檔讀取失敗」。這種情況下,函式無法正常完成它的契約,應該直接拋出錯誤 (Throw an Error),讓上層的錯誤處理機制來捕獲和處理。
null
的舊 API 共存有時,我們無法修改舊的 API。
在這種情況下,現代 JavaScript 提供了強大的防禦工具:
可選串連 (Optional Chaining, ?.
):安全地存取深層嵌套的屬性,如果中途遇到 null
或 undefined
,會立即停止並回傳 undefined
。const address = user?.profile?.address; // 不會爆炸
空值合併運算子 (Nullish Coalescing, ??
):當左側的值是 null
或 undefined
時,提供一個預設值。const displayName = user.name ?? 'Guest';
這些是強大的防禦工具,但更好的做法,永遠是從源頭設計出不需要防禦的 API。
我是否正在埋下一顆 null
地雷,讓未來的開發者去踩?
對於集合,我是否回傳了一個安全的空容器 ([]
) 而不是一顆 null
炸彈?
對於單一物件,我是否回傳了一個清晰的「信封」(Result
物件),強迫呼叫者處理成功與失敗兩種情況?
我的設計,是否讓呼叫端的程式碼變得更簡單、更安全?(這才是重點)
用安全的結構取代 null
和 undefined
:空陣列 []
用於列表,結果物件 { ok, ... }
用於單一物件。
區分失敗與錯誤:用 Result 物件處理可預期的失敗路徑,用 throw Error
處理系統級的意外錯誤。
讓呼叫端寫更少的防守式程式碼,從而降低出錯的機率。
讓資料流與錯誤處理更可預期。
用安全的回傳值取代 空值,這就是在消除你程式碼的臭味。
在Go 更煩了,尤其還有zero value
或是想表達這是optional時.
我曾經做了一個二元屬性 isDraft, 資料庫預設是true
Go bool 預設是false 噗, 就api沒特別給值的話,Go 預設就false
我一開始沒注意到,塞到資料庫想說怎都false 噗
後來只能給*bool, 但又會出現nil XDDD
結果後來這問題怎麼解呢?
我以前開發其他語言好像也有類似問題,後來資料庫設計幾乎都不用太用bool 欄位,都用 0 or 1,算是有點迴避掉這問題了 XDD