iT邦幫忙

2026 iThome 鐵人賽

DAY 24
0
Modern Web

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

Day 24|Modern C#:Pattern Matching、Record 與 Immutability

  • 分享至 

  • xImage
  •  

前面我們已經學過:

Class
Object
Property
Inheritance
Interface
Lambda
LINQ
Exception
async / await

這些都是 C# 的核心能力。

今天要來認識幾個 Modern C# 常見功能,看看如何讓程式碼更清楚地表達:

條件
資料
資料如何改變

今天主要處理三個問題:

我要判斷資料
→ Pattern Matching

我要表示一份資料
→ Record

我不希望直接修改原本資料
→ Immutability

會使用到:

is
Property Pattern
switch expression
record
init
with

今天最重要的觀念:

Modern C# 的價值不在於語法比較新,而是讓程式碼更清楚地表達條件、資料與變化。


1. Pattern Matching 是什麼?

先看一個簡單情境。

假設:

object value = "Hello";

現在想判斷:

value 是不是 string?

可以寫:

if (value is string)
{
    string text =
        (string)value;

    Console.WriteLine(text);
}

這段程式做兩件事:

判斷是不是 string
↓
如果是
↓
再轉型成 string

Modern C# 可以直接:

if (value is string text)
{
    Console.WriteLine(text);
}

這裡:

value is string text

可以直接讀成:

value 是 string 嗎?
↓
如果是
↓
把它當成 string
↓
存進 text

這就是 Pattern Matching 最基本的概念:

判斷資料是否符合某種模式,符合時直接取得需要的資料。


2. 使用 is 判斷型別

例如:

object value = 100;

if (value is int number)
{
    Console.WriteLine(number);
}

如果:

value 是 int

就會得到:

int number

不需要再另外轉型。

Mental Model:

value
↓
是 int 嗎?
↓
是
↓
取得 int number

另外也很常看到:

if (user is null)
{
    Console.WriteLine(
        "找不到使用者"
    );
}

或:

if (user is not null)
{
    Console.WriteLine(
        user.Name
    );
}

可以直接讀成:

user is null
→ user 是 null

user is not null
→ user 不是 null

3. Property Pattern:直接描述 Property 條件

假設有:

class Order
{
    public string Product { get; set; } = "";

    public int Quantity { get; set; }
}

建立:

Order order =
    new Order
    {
        Product = "TXF",
        Quantity = 20
    };

如果只是判斷:

if (order.Quantity > 10)
{
    Console.WriteLine(
        "大量訂單"
    );
}

這樣已經很清楚。

但如果想同時描述:

Product 是 TXF
而且
Quantity > 10

可以使用 Property Pattern:

if (
    order is
    {
        Product: "TXF",
        Quantity: > 10
    }
)
{
    Console.WriteLine(
        "TXF 大量訂單"
    );
}

可以直接理解成:

order 符合以下條件嗎?

Product = TXF
Quantity > 10

所以 Property Pattern 很適合:

直接描述「我要找什麼樣的資料」。


4. switch expression:根據條件得到結果

假設要根據訂單數量分類:

Quantity < 10
→ Small

Quantity < 100
→ Medium

其他
→ Large

一般可以寫:

string level;

if (order.Quantity < 10)
{
    level = "Small";
}
else if (order.Quantity < 100)
{
    level = "Medium";
}
else
{
    level = "Large";
}

沒有錯。

但這段程式真正想做的是:

根據 Quantity 得到一個字串結果。

這時可以使用:

string level =
    order.Quantity switch
    {
        < 10 => "Small",
        < 100 => "Medium",
        _ => "Large"
    };

可以直接讀成:

Quantity < 10
→ Small

Quantity < 100
→ Medium

其他
→ Large

其中:

_

代表:

其他沒有被前面條件符合的情況。


比對順序很重要

switch expression 會:

由上往下比對,遇到第一個符合的 Pattern 就使用該結果。

例如:

Quantity = 5

其實:

5 < 10
→ true

5 < 100
→ 也是 true

但程式會先遇到:

< 10 => "Small"

所以結果是:

Small

後面的條件不再繼續比對。

Mental Model:

由上往下判斷
↓
找到第一個符合的 Pattern
↓
使用它的結果

5. Pattern Matching 什麼時候適合使用?

不用看到 if 就全部改成 Pattern Matching。

例如:

if (order.Quantity > 10)

本來就很清楚。

不用刻意改成:

if (order is { Quantity: > 10 })

Pattern Matching 比較適合:

判斷型別
→ is

判斷 null
→ is null / is not null

同時描述多個 Property
→ Property Pattern

根據多種 Pattern 得到結果
→ switch expression

所以真正的原則是:

選擇最能清楚表達條件的寫法。


6. 為什麼需要 record?

前面我們很常使用:

class

例如:

class User
{
    public string Name { get; set; } = "";

    public int Age { get; set; }
}

這完全沒有問題。

如果這個 Object:

有狀態
有行為
狀態會持續改變

Class 很適合。

但有些 Type 主要只是:

保存資料
傳遞資料
表示某個結果

例如:

API Request
API Response
DTO
查詢結果

這種以資料為主的 Type,就可以認識:

record

7. 建立一個 record

例如:

public record User(
    string Name,
    int Age
);

只需要這樣,就可以:

User user =
    new User(
        "Tom",
        20
    );

使用:

Console.WriteLine(
    user.Name
);

Console.WriteLine(
    user.Age
);

所以可以先把:

record

理解成:

很適合用來表示「一份資料」的 Type。

例如:

User
→ Name
→ Age

我們比較關心:

這份資料裡有什麼?

8. Positional Record 和 init

剛才:

public record User(
    string Name,
    int Age
);

這種寫法稱為:

Positional Record

C# 會幫我們產生對應的 Property。

這些 Property 預設不是一般的:

set;

而是類似:

init;

所以建立:

User user =
    new User(
        "Tom",
        20
    );

之後,不能直接:

user.Age = 21;

可以先理解成:

建立 Record
↓
設定資料
↓
建立完成
↓
不能直接重新指定這些 Property

這也會自然帶到後面的:

Immutability

9. Record 的 Value Equality

record 還有一個重要特色:

Value Equality
→ 依資料內容判斷是否相等

例如:

User user1 =
    new User(
        "Tom",
        20
    );

User user2 =
    new User(
        "Tom",
        20
    );

比較:

Console.WriteLine(
    user1 == user2
);

結果:

True

因為:

user1
Name = Tom
Age = 20

user2
Name = Tom
Age = 20

兩份 Record 的資料內容相同。


和一般 Class 比較

可以先理解成:

一般 class
→ 預設比較是不是同一個 Object / Reference

record
→ 預設依資料內容比較

所以兩個 Class Object:

內容一樣
≠
預設就一定相等

而兩個 Record:

資料內容相同
→ 預設可以視為相等

要注意:

這裡說的是預設行為。Class 也可以自行改寫 Equality。

今天不需要深入底層實作。


10. Class 和 Record 怎麼選?

可以先用這個方向判斷。

Class

比較適合:

物件有明確行為
狀態會持續改變
強調 Object Identity

例如:

ShoppingCart
BankAccount
GamePlayer

這些 Object 不只是保存資料,還可能有:

AddItem()
Deposit()
Withdraw()
Attack()

Record

比較適合:

主要保存資料
主要傳遞資料
希望依內容比較
常搭配較少變動的資料設計

例如:

DTO
API Request
API Response
查詢結果

可以先記:

Class
→ 偏向「物件與行為」

Record
→ 偏向「資料」

這不是絕對規則。

真正還是要看:

這個 Type 在系統裡扮演什麼角色。


11. Record 就等於 Immutable 嗎?

這裡要特別注意:

record 很適合搭配 Immutable Design,但 record 本身不代表所有資料一定完全不可變。

所以不要直接記成:

record
=
Immutable

更好的理解是:

record
→ 以資料為中心的 Type

init
→ 限制 Property 建立後重新指定

with
→ 根據原本資料建立修改後的新資料

這幾個概念搭配起來,非常適合:

Immutable Design

12. 什麼是 Immutability?

Immutability:

Immutable
→ 不可變

可以先理解成:

Object 建立完成後,不隨意直接修改原本資料。

假設一個 Object 到處都能修改:

Method A
→ 修改 Name

Method B
→ 修改 Age

Method C
→ 又修改 Name

程式變大後,很容易產生:

這個值到底在哪裡被改掉?

Immutable Design 的想法是:

原本資料
↓
保留

需要不同資料
↓
建立新的資料

這樣資料變化通常會更容易追蹤。


13. init:只在初始化時設定

一般 Property:

public string Name
{
    get;
    set;
}

建立後仍可以:

user.Name = "John";

如果希望 Property 只能在初始化階段設定,可以使用:

init

例如:

public record User
{
    public string Name
    {
        get;
        init;
    } = "";

    public int Age
    {
        get;
        init;
    }
}

建立時:

User user =
    new User
    {
        Name = "Tom",
        Age = 20
    };

可以設定。

但建立完成後:

user.Name = "John";

就不能直接重新指定。

所以:

init
↓
初始化時可以設定
↓
初始化完成後不能直接重新指定

14. 如果資料真的需要變更呢?

Immutability 並不是:

資料永遠不能變

而是:

不要直接修改原本資料,而是建立一份新的資料。

例如:

public record User(
    string Name,
    int Age
);

建立:

User user =
    new User(
        "Tom",
        20
    );

現在 Tom 生日了:

Age
20
→
21

可以使用:

with

建立新資料:

User newUser =
    user with
    {
        Age = 21
    };

現在:

user
→ Name = Tom
→ Age = 20

newUser
→ Name = Tom
→ Age = 21

原本的:

user

沒有被修改。


15. with 怎麼理解?

可以把:

User newUser =
    user with
    {
        Age = 21
    };

理解成:

以 user 為基礎
↓
建立一份新的資料
↓
大部分內容保持相同
↓
Age 改成 21
↓
得到 newUser

比較:

Mutable 思維

原本 user
↓
直接修改
↓
Age = 21

和:

Immutable 思維

原本 user
↓
保留

建立 newUser
↓
Age = 21

所以 with 很適合表達:

根據原本資料,建立修改後的新版本。


16. Record + with 完整範例

建立:

public record Order(
    string Product,
    int Quantity
);

原本:

Order order =
    new Order(
        "TXF",
        10
    );

現在希望:

Product 還是 TXF
Quantity 改成 20

可以:

Order newOrder =
    order with
    {
        Quantity = 20
    };

輸出:

Console.WriteLine(
    order.Quantity
);

Console.WriteLine(
    newOrder.Quantity
);

結果:

10
20

也就是:

order
→ 原本資料
→ Quantity = 10

newOrder
→ 新資料
→ Quantity = 20

17. Record 在 ASP.NET Core 會在哪裡看到?

下一階段進入 ASP.NET Core 後,會常遇到:

Request
Response
DTO

例如建立使用者的 Request:

public record CreateUserRequest(
    string Name,
    int Age
);

Client 可能傳來:

{
  "name": "Tom",
  "age": 20
}

後端可以把資料接成:

CreateUserRequest

可以先理解成:

Client JSON
↓
ASP.NET Core
↓
Request Model / DTO
↓
C# Object

record 很適合用來表示:

Request、Response、DTO 這類以資料傳遞為主的模型。

但要注意:

DTO
→ 可以使用 class

DTO
→ 也可以使用 record

並不是 ASP.NET Core 規定 DTO 一定要用 record。

之後我們會正式學:

Model Binding
DTO
Validation

18. 把今天三個概念放在一起

假設:

public record Order(
    string Product,
    int Quantity
);

建立:

Order order =
    new Order(
        "TXF",
        20
    );

使用 Pattern Matching:

string level =
    order switch
    {
        { Quantity: < 10 }
            => "Small",

        { Quantity: < 100 }
            => "Medium",

        _
            => "Large"
    };

如果需要新的訂單資料:

Order newOrder =
    order with
    {
        Quantity = 100
    };

這裡同時用了:

Pattern Matching
→ 判斷資料符合什麼條件

Record
→ 表示一份訂單資料

with
→ 建立修改後的新資料

這就是今天三個主題之間的關係。


今日練習

練習一|is Pattern

建立:

object value = "Hello";

使用:

is

判斷是不是 string。

如果是,輸出:

Hello

練習二|Property Pattern

建立:

Order order =
    new Order(
        "TXF",
        20
    );

判斷:

Product == TXF
而且
Quantity > 10

符合時輸出:

TXF 大量訂單

練習三|switch expression

根據:

Quantity < 10
→ Small

Quantity < 100
→ Medium

其他
→ Large

使用:

switch expression

建立:

string level

並思考:

如果 Quantity = 5,為什麼結果是 Small,而不是 Medium?


練習四|Record 與 with

建立:

public record User(
    string Name,
    int Age
);

建立:

Tom
20

接著使用:

with

建立另一份:

Tom
21

確認:

原本 user
→ Tom / 20

新的 newUser
→ Tom / 21

Day 24 小結

今天學習三個 Modern C# 重要方向。

Pattern Matching

is
Property Pattern
switch expression

核心:

更清楚地描述「資料符合什麼條件」。


Record

record
→ 適合以資料為中心的 Type

特色之一:

Value Equality
→ 預設依資料內容比較

核心:

更清楚地表示「這是一份資料」。


Immutability

init
→ 初始化時設定資料

with
→ 建立修改後的新資料

核心:

不直接修改原本資料
↓
需要變化
↓
建立新的資料

要注意:

record
≠
完全 Immutable

而是:

record
+
init
+
with

很適合用來實作 Immutable Design。


整篇最重要的一句話:

Modern C# 的價值不在於語法比較新,而是讓程式碼更清楚地表達條件、資料與變化。


上一篇
Day 23|Async 實務:多個非同步工作怎麼處理?
系列文
現在就學C# 與 ASP.NET Core 共 24 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言