iT邦幫忙

2025 iThome 鐵人賽

DAY 23
1
Software Development

消除你程式碼的臭味系列 第 23

Day 23- 空值處理:別回傳 null,用更安全的回應

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250903/20124462P1N8QjGguI.png

消除你程式碼的臭味 Day 23- 空值處理:別回傳 null,用更安全的回應

Tony Hoare,null 的發明者,稱它為「十億美元的錯誤」。
nullundefined 是程式設計史上最多產的 bug 來源。

一個回傳 nullundefined 的函式,就像一個隨手把地雷埋在程式碼裡然後走開的工程師。
他把排雷的責任,丟給了每一個未來會與這段程式碼互動的人。

用空陣列、明確的結果物件或拋出錯誤,別丟個 null 讓人猜。

null 假裝自己是一個正常的值,但當你試圖對它做任何事情時——比如 user.name——它就會立刻爆炸,給你一個 TypeError: Cannot read properties of null

經典案例:回傳 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) {
    // ... 沒完沒了
  }
}

https://ithelp.ithome.com.tw/upload/images/20250925/201244627GxIbj6iKZ.png

  • 不負責任: 「找不到」是一種可能情況,但卻選擇把處理這個問題的負擔,轉嫁給了成百上千個呼叫者。

  • 依賴人類脆弱的記憶: 你必須記得去檢查空值>

  • 它的型別不一致: 這個函式的回傳型別是什麼?是 User 或者 null或者 undefined。這讓靜態分析和開發者的心智負擔都大大增加。

回傳更安全的結構

專業的程式設計師,會設計一個預設安全 (safe by default) 的 API。
回傳一個結構,讓呼叫者即使不加檢查,程式碼也不會爆炸。

方案一:對於集合,回傳「空的」而不是「沒有」Empty Collection Pattern

如果你的函式回傳一個列表(陣列),在找不到任何東西時,永遠回傳一個空陣列 []

// 🟢 好味道:絕對安全,符合直覺。
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."

https://ithelp.ithome.com.tw/upload/images/20250925/20124462LBYcMXZ6Gu.png

這個模式是處理集合問題的黃金準則。

方案二:對於單一物件,回傳一個明確的「信封」Result / Option Pattern

如果函式是查找單一物件,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}`);
}

https://ithelp.ithome.com.tw/upload/images/20250925/20124462zrZBIlPvZK.png

這個模式把一個容易被遺忘的執行時錯誤null 爆炸),轉變成了一個必須處理的邏輯分支

API 設計的靈魂拷問

重要的區分:可預期的失敗 vs. 意外的錯誤

Result 模式雖然強大,但並非是萬能解法。
我們需要區分兩種失敗:

  • 可預期的失敗 (Failure):這是業務邏輯的一部分,是「正常」的失敗路徑。例如「使用者不存在」、「密碼錯誤」。使用 Result 物件是完美的選擇

  • 意外的錯誤 (Error/Exception):這是系統層級的、非預期的異常。例如「資料庫連線中斷」、「設定檔讀取失敗」。這種情況下,函式無法正常完成它的契約,應該直接拋出錯誤 (Throw an Error),讓上層的錯誤處理機制來捕獲和處理。

你無法選擇:如何與充滿 null 的舊 API 共存

有時,我們無法修改舊的 API。
在這種情況下,現代 JavaScript 提供了強大的防禦工具:

  • 可選串連 (Optional Chaining, ?.):安全地存取深層嵌套的屬性,如果中途遇到 nullundefined,會立即停止並回傳 undefined
    const address = user?.profile?.address; // 不會爆炸

  • 空值合併運算子 (Nullish Coalescing, ??):當左側的值是 nullundefined 時,提供一個預設值。
    const displayName = user.name ?? 'Guest';

這些是強大的防禦工具,但更好的做法,永遠是從源頭設計出不需要防禦的 API。

在你設計一個函式的回傳值時,問自己:

  1. 我是否正在埋下一顆 null 地雷,讓未來的開發者去踩?

  2. 對於集合,我是否回傳了一個安全的空容器 ([]) 而不是一顆 null 炸彈?

  3. 對於單一物件,我是否回傳了一個清晰的「信封」(Result 物件),強迫呼叫者處理成功與失敗兩種情況?

  4. 我的設計,是否讓呼叫端的程式碼變得更簡單、更安全?(這才是重點)

今日重點

  • 用安全的結構取代 nullundefined:空陣列 [] 用於列表,結果物件 { ok, ... } 用於單一物件。

  • 區分失敗與錯誤:用 Result 物件處理可預期的失敗路徑,用 throw Error 處理系統級的意外錯誤。

  • 讓呼叫端寫更少的防守式程式碼,從而降低出錯的機率。

  • 讓資料流與錯誤處理更可預期。

用安全的回傳值取代 空值,這就是在消除你程式碼的臭味。


上一篇
Day 22- 輸入檢查:在處理前先驗證
系列文
消除你程式碼的臭味23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
雷N
iT邦研究生 1 級 ‧ 2025-09-25 18:09:14

在Go 更煩了,尤其還有zero value
或是想表達這是optional時.

我曾經做了一個二元屬性 isDraft, 資料庫預設是true
Go bool 預設是false 噗, 就api沒特別給值的話,Go 預設就false
我一開始沒注意到,塞到資料庫想說怎都false 噗
後來只能給*bool, 但又會出現nil XDDD

Sunny.Cat iT邦新手 2 級 ‧ 2025-09-25 23:35:01 檢舉

結果後來這問題怎麼解呢?
我以前開發其他語言好像也有類似問題,後來資料庫設計幾乎都不用太用bool 欄位,都用 0 or 1,算是有點迴避掉這問題了 XDD

我要留言

立即登入留言