Exception Handling

窗外開始下雨了,輕輕敲在窗上的雨聲,像記憶敲著玻璃,提醒你今天又過去了一天。

房間裡只亮著一盞檯燈,桌上的咖啡溫度早已散去。你盯著螢幕,一行行程式碼像星辰般閃爍,帶著某種秩序,也藏著未解的謎題。

這時候,它又來了。

System.NullReferenceException, Object reference not set to an instance of an object.

我滄桑的搖了搖頭,示意自己又大意了一回



🏔️ NullReferenceException

當你試圖透過一個根本沒有指向任何物件的參考(null)來存取資料或方法,.NET 就會丟出 NullReferenceException。

每一個「物件變數」本質上是一個指向記憶體位置的參考(Reference)。

舉例來說:

1
2
3
4
5

Person person = new Person();
Person person = null;
person.Name //// ❌ NullReferenceException。

person 就像你手上拿是地址(像 Google Maps 上的定位點),而 person.Name 表示請你開車去那個地址,然後進門找「那個人的名字」,如果 person = null,則相當於,手上的地圖定位點是一坨空白,登愣!



🏔️ 維護團隊專案的心態

對物件的存在永遠不要理所當然,設計時應「懷疑一切可能為 null」,現實常見的情境如

  • 從資料庫抓不到資料卻直接使用
  • DI 注入失敗或沒註冊物件
  • JSON deserialize 成 null
  • Service 回傳 null 未做檢查

以及其他花式資料遺失的情境,當你 Exception 碰久了,會開始漸漸築起警戒心,但這也不是說,都是誰的錯,而是設計程式隨時需要思考資料的處理流向與處理的必要性,小小的 person.Name ,我們可能需要想,資料怎麼來的,又要怎麼去,是使用者輸入有誤嗎,我該抓出錯誤回報什麼錯誤訊息?…

在面對 Null Reference Issue 時,我們可以透過不同的面向迎擊
例如專案可以開啟 Nullable Reference Types,編譯器會跑出那些小蚯蚓來提示你程式碼的哪個部位可能有 null

撰寫邏輯時,如何透過語法糖的使用例如 ?.(Null Conditional Operator) 和 ?? (Null Coalescing Operator) 作為安全存取與設定預設值的工具

總而言之,我們要避免過度信任外部資料,API、DB 回傳的值要小心檢查、建構函式就初始化好物件 不要等到用的時候才想起來、運用 DI(Dependency Injection)時,明確指出依賴,並善用單元測試,檢查極端與例外情況,提早發現問題



🏔️ 到處用 ?. 安心嗎?還是潛藏危機?

在寫 C# 的時候,可以用 ?.(null conditional operator)來「擋錯」,一不小心就寫了一串:

1
2
3

var result = data?.List?.FirstOrDefault()?.Name?.Trim()?.ToLower();

看起來好像「防錯做滿了」,但這真的好嗎?

🔹 回到問題的本質,我們應該問的是:

「為什麼這個東西會是 null?」

而不是

「怎麼樣加 ?. 才不會噴錯?」

合理使用 ?. 的情境,是你明確知道某個物件有可能是 null,例如:

  • 來自外部 API 回傳的資料
  • 資料庫查詢結果(尤其是左外連接)
  • 前端表單的非必填欄位
  • Cookie、Session、QueryString 的值

這些都是你無法掌控來源時,安全防禦的一環。

⚠️ 不建議無腦加 ?. 的狀況

1
2
3

var discount = order?.Coupon?.DiscountAmount ?? 0;

原本是預期:沒有 Coupon 就不要打折

但問題來了:有些 order 資料在還沒綁定前,Coupon 是 null,應該套用預設折扣
但有時候是訂單不知道為甚麼資料不見了卻還是套用預設折扣,反而是走到後面然後資料不齊全到狀況

我們改成更清楚、具判斷意圖的寫法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

if (order == null)
{
// 系統層錯誤,代表訂單不存在,這不應發生
logger.LogError("訂單為 null,無法計算折扣");
throw new InvalidOperationException("訂單資訊異常");
}
else if (order.Coupon == null)
{
// 訂單存在,但沒有優惠券,屬於正常邏輯流程
logger.LogInformation("未套用優惠券,使用預設折扣");
discount = defaultDiscount;
}
else
{
// 有優惠券,正常折扣流程
discount = order.Coupon.DiscountAmount;
}

我們是否不小心用 null propagation 來模糊掉異常狀態,反而導致後續釐清問題的困難



🏔️ 預設值

當我們已經確定某個資料為 null 是合理的情況,也就是說:

「這筆資料可以沒有,即使缺失也不應影響整體流程。」

這時候,使用預設值(例如 ?? 或 GetValueOrDefault())來處理,就是一種非常合適的容錯策略。

1
2
3

var userName = user?.Profile?.Name ?? "訪客";

這段程式碼的意圖是:在未登入時顯示「訪客」。這屬於正常的 UX fallback,因為匿名瀏覽是被允許的情境。

但前提是 —— 你已確認 user == null 就代表「未登入」,而不是系統異常或資料遺失。如果你清楚這點,那麼這樣使用 ?? 是合理的。否則可能發生使用者登入,結果畫面還是顯示訪客的情況!

再來一個例子

在行銷活動中,有些活動可能只設條件與優惠,而沒有填寫說明文字。這樣的缺值在業務上是被接受的。此時給一個親切的預設值,如「無備註」,會讓 UI 更完整,也不會誤導使用者。

1
2
3

var remark = campaign.Description ?? "(無備註)";

⚠️ 注意:有些人會習慣「看到 null 就補值」,但如果這個補的值沒有業務語意,只為了「不報錯」,反而容易造成誤導。

1
2
3

var total = order.TotalAmount ?? 0;

你知道為什麼 TotalAmount 是 null 嗎?
是訂單根本還沒完成?還是金額真的為 0?
這樣 fallback 可能讓後續報表失真、財務混亂!



🏔️ string interpolation

當我們在使用 字串插值(string interpolation) 的語法,例如 $”{xxx}”,它看起來像是語法糖,但實際上背後經過了編譯器的轉換與優化。如果插入的是 nullable value type(例如 int?、bool?、DateTime?),那麼整個流程會非常貼心地自動處理 null 的情況,避免 NullReferenceException 發生。

我們看一個例子:

1
2
3
4
5

int? age = null;
Console.WriteLine($"Age: {age}");

//// Age:

它不會拋出例外,也不會印出 “null”。這是因為 C# 的編譯器會在背後幫你處理成這樣的邏輯:

1
2
3

age.HasValue ? age.Value.ToString() : ""

也就是說等於

1
2
3

Console.WriteLine("Age: " + (age.HasValue ? age.Value.ToString() : ""));

雖然我們知道做輸出時,會 ToString, 但沒想到他會幫你做防呆吧!



🏔️ HasValue & .Value

使用 Nullable(例如 int?、bool?、DateTime?)時,HasValue 和 .Value 是兩個密不可分的屬性,但它們的角色截然不同,理解它們的搭配使用時機,可以幫助我們寫出更安全、更具掌控力的程式。

在底層一點點,Nullable 是個特殊的結構體(struct),內部實作類似這樣:

1
2
3
4
5
6
7
8
9
10

public struct Nullable<T> where T : struct
{
private bool hasValue;
private T value;

public bool HasValue => hasValue;
public T Value => hasValue ? value : throw new InvalidOperationException("Nullable object must have a value.");
}

這段程式碼告訴我們一件重要的事:
👉 你不能無條件地使用 .Value,否則可能會拋出 InvalidOperationException。

在你準備使用 .Value 前,應該先使用 HasValue 做保險。

1
2
3
4
5
6
7
8
9
10
11
12

int? score = GetUserScore();

if (score.HasValue)
{
Console.WriteLine($"User Score: {score.Value}");
}
else
{
Console.WriteLine("User has no score yet.");
}

還有一招是

1
2
3
4
5

int? age = null;
int value = age.GetValueOrDefault(); // 預設值為 0
int custom = age.GetValueOrDefault(99); // 預設值為 99



🏔️ is null == null

基本語意是這樣

1
2
3
4

x == null //// 呼叫 operator == 比較
x is null //// 判斷物件是否是 null

🔹 x == null

這是一個「運算子多載(operator overloading)」的呼叫。
也就是說,如果 x 是某個類別的實例,而這個類別有自定義 operator ==,那麼執行的就是該方法。意味著 == null 可以被重寫、被干擾!

🔹 x is null

這是 C# 7.0 以後提供的 Pattern Matching Null Test,是一種語法層級的 null 判斷,不會被 operator 多載影響。

也就是說,它永遠是單純的「這個參考是否為 null」。

x is null 是語言本身的保證;x == null 則是你或別人可以動手腳的合約。



☘️ 結語。

如何分辨 x == null 與 x is null 的本質差異?

為何 ?. 不是萬靈丹,而應該搭配清楚的判斷邏輯使用?

如何用 ?? 和 GetValueOrDefault() 做出真正貼合情境的預設值?

還有那個最根本的提醒 —— 在使用 .Value 前,請先問自己:這真的有值嗎?

在這 Debug 的深夜裡,我們怎麼與 null 相處

🕯窗外的雨停了,城市的輪廓在路燈下緩緩清晰。


你儲存了檔案,啜了口早已冰涼的咖啡,心裡卻因為解開一個小謎題而泛起微光。這不是一場戰鬥,而是一種與混亂共舞的優雅。


下一次遇見 Null,你會更有感覺,也更從容。🌙