Day 11 我們透過 Encapsulation 與 Property,讓 Class 開始管理自己的資料。
例如:
class Order
{
public string Product { get; private set; }
public Order(string product)
{
Product = product;
}
}
每一筆 Order 都必須有:
Product
但如果我們加入:
Note
情況就不一樣。
因為一筆訂單:
可能有 Note
也可能沒有 Note
這時就會遇到今天的問題:
如果一筆資料可能不存在,在 C# 裡要怎麼表示與處理?
今天會認識:
null
string 與 string?
?.
??
null 是什麼?假設訂單有一個備註:
string note = "Urgent";
可以簡單理解成:
note
↓
"Urgent"
現在 note 有資料可以使用。
但如果這筆訂單沒有 Note,要怎麼表示?
在 C# 中,可以使用:
null
可以先把 null 理解成:
目前沒有資料可以使用。
對 Reference Type 更精確地說:
null** 表示這個 Reference 目前沒有參考到任何 Object。**
例如:
有資料
note
↓
"Urgent"
沒有資料
note
↓
null
目前不用深入 Reference 的底層細節。
先理解:
有 Object
→ 可以透過 Reference 使用它
null
→ 目前沒有參考到 Object
null 不是空字串這點很重要。
string name = "";
代表:
有一個 String,只是內容是空的。
而:
string? name = null;
代表:
name目前沒有參考到 String Object。
所以:
""
→ 有 String
→ 只是內容是空的
null
→ 沒有參考到 String Object
同樣地:
0
→ 有數值,只是值是 0
false
→ 有 Boolean 值,只是值是 false
null
→ 沒有參考到 Object
目前先記住:
null不是""、不是0,而是目前沒有參考到 Object。
string 與 string?現在知道 null 是什麼之後,再來看 Type。
假設:
Product
→ 一定要有
Note
→ 可以沒有
可以寫成:
string product = "TXF";
string? note = null;
這裡:
string
與:
string?
只差一個:
?
但代表不同的設計意圖。
| Type | 設計意圖 |
|---|---|
string |
預期不為 null |
string? |
允許為 null |
所以:
string product = "TXF";
代表:
product
→ 預期一定有值
而:
string? note = null;
代表:
note
→ 可以有值
→ 也允許為 null
像:
string?
這種寫法稱為:
Nullable Reference Type(可為 Null 的參考型別)
它讓我們直接在 Type 上表達:
這個資料是否允許
null?
例如:
string product = null;
在 Nullable Reference Types 啟用時,Compiler 會提出 Warning。
因為:
string
→ 預期不為 null
但我們卻指定:
null
兩者的設計意圖不一致。
如果資料本來就允許不存在:
string? note = null;
就應該明確使用:
string?
因此可以先記:
string
→ 預期有值
string?
→ 允許 null
既然:
string? note = null;
代表 note 可能是 null。
如果我們直接:
Console.WriteLine(
note.Length
);
就會出現問題。
因為現在:
note
↓
null
↓
沒有參考到 String Object
但程式卻要求:
取得 Length
也就是:
沒有 Object
↓
卻要求存取它的成員
Compiler 會先提出 Warning。
如果 Runtime 真的在 note 為 null 時執行這段程式,就可能發生:
NullReferenceException
所以:
null** 本身不一定是錯誤,真正危險的是沒有處理null,卻直接透過它存取成員。**
為什麼 Compiler 知道:
note.Length
可能有問題?
因為:
string? note
已經告訴 Compiler:
note
→ 可能是 null
Compiler 會根據 Type 與程式流程,判斷目前的 Variable 是否可能為 null。
這稱為:
Null-state Analysis(Null 狀態分析)
可以先理解成:
Compiler
↓
分析 Variable
↓
┌───────┴───────┐
↓ ↓
Not Null Maybe Null
↓ ↓
正常使用 可能提出 Warning
例如:
string? note = null;
目前 Compiler 會認為:
note
↓
Maybe Null
所以直接:
note.Length
就可能出現 Warning。
重點是:
Compiler 不只看 Type,也會分析目前的程式流程。
最直接的處理方式就是:
先確認它不是
null。
例如:
if (note is not null)
{
Console.WriteLine(
note.Length
);
}
原本:
note
↓
Maybe Null
經過:
note is not null
如果條件成立:
note
↓
Not Null
所以在 if 裡:
note.Length
就可以安全使用。
流程:
note
↓
是 null?
│
├── Yes → 不使用
│
└── No
↓
Not Null
↓
note.Length
這種先確認是否為 null 的方式稱為:
Null Check
nullDay 9 學過 Early Return。
同樣可以使用:
void PrintNote(string? note)
{
if (note is null)
{
return;
}
Console.WriteLine(
note.Length
);
}
流程:
note
↓
是 null?
│
├── Yes → return
│
└── No
↓
Not Null
↓
note.Length
經過 Null Check 後,Compiler 就知道:
後面的 note
→ Not Null
?.:有值才存取有些情況不需要寫完整的 if。
如果只是希望:
有值就使用,沒有值就不要存取。
可以使用:
Console.WriteLine(
note?.ToUpper()
);
?. 稱為:
Null-conditional Operator(Null 條件運算子)
可以理解成:
note
↓
有值?
│
├── Yes → 執行 ToUpper()
│
└── No → 結果為 null
如果:
note = "Urgent"
則:
"Urgent"
↓
ToUpper()
↓
"URGENT"
如果:
note = null
就不會直接呼叫 ToUpper()。
因此:
?.** 可以在 Reference 可能為null時,安全地存取成員。**
??:null 時使用替代值如果希望:
Note 沒有值時,顯示
"No Note"。
可以使用:
string displayNote =
note ?? "No Note";
?? 稱為:
Null-coalescing Operator(Null 合併運算子)
可以理解成:
note
↓
有值?
│
├── Yes → 使用 note
│
└── No → 使用 "No Note"
例如:
string? note = null;
string displayNote =
note ?? "No Note";
Console.WriteLine(displayNote);
結果:
No Note
?. 與 ?? 可以一起使用例如:
有 Note 就轉成大寫,沒有就顯示
"NO NOTE"。
可以:
string displayNote =
note?.ToUpper()
?? "NO NOTE";
流程:
note
↓
?.ToUpper()
↓
有值 → 轉成大寫
沒值 → null
↓
?? "NO NOTE"
↓
null 時使用替代值
如果:
note = "Urgent"
結果:
URGENT
如果:
note = null
結果:
NO NOTE
?.、?? 怎麼選?初學階段可以先這樣判斷:
| 寫法 | 適合情況 |
|---|---|
if (x is not null) |
需要控制程式流程 |
x?.Member |
有值才存取成員 |
x ?? value |
null 時使用替代值 |
例如:
if (note is not null)
{
Console.WriteLine(note.Length);
}
適合:
需要根據 null
決定後續流程
Console.WriteLine(
note?.ToUpper()
);
適合:
有值才操作
note ?? "No Note";
適合:
沒有值時
提供替代值
不需要刻意追求最短的寫法,選擇最能清楚表達程式意圖的方式即可。
?假設:
public string Product { get; private set; }
出現 Nullable Warning。
不要第一時間改成:
public string? Product { get; private set; }
因為:
string Product
→ Product 預期一定存在
而:
string? Product
→ Product 允許不存在
兩者代表不同的資料設計。
如果規則是:
每一筆 Order 都必須有 Product。
那麼只是為了消除 Warning 而改成:
string?
反而改變了原本的 Type Design。
所以遇到 Nullable Warning 時,先問:
這個資料在設計上真的允許沒有值嗎?
Nullable Reference Types 主要在編譯階段幫助 Compiler 分析可能的 null 風險。
可以理解成:
Nullable Reference Types
↓
Compile Time
↓
Compiler 根據 Type
與程式流程進行分析
↓
Null-state Analysis
↓
提前提醒可能的 null 風險
例如:
string
→ Compiler 預期不為 null
string?
→ Compiler 知道允許 null
但要注意:
Nullable Reference Types 不代表 Runtime 永遠不可能出現
null。
它主要是在 Compile Time 協助我們提早發現問題。
所以:
Nullable Reference Types 是提高 Null Safety 的重要機制。
現在把今天學到的內容放回 Order。
一筆 Order 有:
Product
→ 一定要有
Note
→ 可以沒有
因此可以設計成:
class Order
{
public string Product { get; private set; }
public string? Note { get; private set; }
public Order(
string product,
string? note)
{
Product = product;
Note = note;
}
public string GetDisplayNote()
{
return Note ?? "No Note";
}
}
現在從 Type 就可以知道:
Order
│
├── Product
│ ↓
│ string
│ ↓
│ 預期一定存在
│
└── Note
↓
string?
↓
允許不存在
沒有 Note:
Order order =
new Order("TXF", null);
輸出:
Console.WriteLine(order.Product);
Console.WriteLine(order.GetDisplayNote());
結果:
TXF
No Note
有 Note:
Order order =
new Order("TXF", "Urgent");
這就是 Nullable Reference Types 很重要的設計價值:
讓資料是否允許不存在,直接成為 Type Design 的一部分。
! 即可有時候會看到:
note!.Length
其中:
!
稱為:
Null-forgiving Operator
它是在告訴 Compiler:
我確定這裡不是
null,不要提出 Nullable Warning。
但要注意:
!不會真的處理null。
例如:
string? note = null;
Console.WriteLine(
note!.Length
);
Nullable Warning 可能消失。
但:
note
↓
null
Runtime 仍然可能發生:
NullReferenceException
所以目前先記住:
!
→ 不處理 null
→ 只影響 Compiler Warning
因此:
不要只是為了消除 Warning 就直接加入
!。
今天從一個實際的問題開始:
Note
可能有資料
也可能沒有資料
如果目前沒有參考到 Object:
null
Nullable Reference Types 可以讓我們透過 Type 表達:
string
→ 預期不為 null
string?
→ 允許為 null
整體流程:
資料可能不存在
↓
null
↓
string / string?
↓
Nullable Reference Types
↓
可能是 null
↓
Compiler
Null-state Analysis
↓
Null Check / ?. / ??
處理 null 的核心方式:
| 寫法 | 用途 |
|---|---|
x is not null |
明確檢查 null |
x?.Member |
有值才存取 |
x ?? value |
null 時使用替代值 |
最後記住三件事:
第一,
string與string?不只是語法不同,也是在表達資料是否允許不存在。
第二,Compiler 會透過 Null-state Analysis,在編譯階段提醒可能的
null風險。
第三,不要看到 Nullable Warning 就直接加入
?或!,先確認資料在設計上到底允不允許不存在。
Nullable Reference Types 最重要的價值就是:
讓「這個資料能不能沒有值」成為 Type Design 的一部分。