iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
Modern Web

現在就學C# 與 ASP.NET Core系列 第 12 篇

Day 12|Nullable Reference Types:理解 null 與安全存取

  • 分享至 

  • xImage
  •  

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?
  • Nullable Reference Types
  • NullReferenceException
  • Null-state Analysis
  • Null Check
  • ?.
  • ??

1. 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。


2. 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

3. Nullable Reference Types

像:

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

4. NullReferenceException

既然:

string? note = null;

代表 note 可能是 null。

如果我們直接:

Console.WriteLine(
    note.Length
);

就會出現問題。

因為現在:

note
 ↓
null
 ↓
沒有參考到 String Object

但程式卻要求:

取得 Length

也就是:

沒有 Object
    ↓
卻要求存取它的成員

Compiler 會先提出 Warning。

如果 Runtime 真的在 note 為 null 時執行這段程式,就可能發生:

NullReferenceException

所以:

null** 本身不一定是錯誤,真正危險的是沒有處理 null,卻直接透過它存取成員。**


5. Null-state Analysis

為什麼 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,也會分析目前的程式流程。


6. Null Check

最直接的處理方式就是:

先確認它不是 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


Early Return 也可以處理 null

Day 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

7. ?.:有值才存取

有些情況不需要寫完整的 if。

如果只是希望:

有值就使用,沒有值就不要存取。

可以使用:

Console.WriteLine(
    note?.ToUpper()
);

?. 稱為:

Null-conditional Operator(Null 條件運算子)

可以理解成:

note
 ↓
有值?
 │
 ├── Yes → 執行 ToUpper()
 │
 └── No  → 結果為 null

如果:

note = "Urgent"

則:

"Urgent"
   ↓
ToUpper()
   ↓
"URGENT"

如果:

note = null

就不會直接呼叫 ToUpper()。

因此:

?.** 可以在 Reference 可能為 null 時,安全地存取成員。**


8. ??: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

9. Null Check、?.、?? 怎麼選?

初學階段可以先這樣判斷:

寫法 適合情況
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";

適合:

沒有值時
提供替代值

不需要刻意追求最短的寫法,選擇最能清楚表達程式意圖的方式即可。


10. 不要看到 Warning 就直接加 ?

假設:

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 時,先問:

這個資料在設計上真的允許沒有值嗎?


11. Nullable Reference Types 主要作用在 Compile Time

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 的重要機制。


12. 回到 Order Class

現在把今天學到的內容放回 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 就直接加入 !。

Day 12 小結

今天從一個實際的問題開始:

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 的一部分。


上一篇
Day 11|Encapsulation 與 Property:讓 Class 管理自己的資料
下一篇
Day 13|Inheritance:用繼承表達 Type Relationship
系列文
現在就學C# 與 ASP.NET Core 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言