前面我們已經學過:
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# 的價值不在於語法比較新,而是讓程式碼更清楚地表達條件、資料與變化。
先看一個簡單情境。
假設:
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 最基本的概念:
判斷資料是否符合某種模式,符合時直接取得需要的資料。
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
假設有:
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 很適合:
直接描述「我要找什麼樣的資料」。
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
↓
使用它的結果
不用看到 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
所以真正的原則是:
選擇最能清楚表達條件的寫法。
record?前面我們很常使用:
class
例如:
class User
{
public string Name { get; set; } = "";
public int Age { get; set; }
}
這完全沒有問題。
如果這個 Object:
有狀態
有行為
狀態會持續改變
Class 很適合。
但有些 Type 主要只是:
保存資料
傳遞資料
表示某個結果
例如:
API Request
API Response
DTO
查詢結果
這種以資料為主的 Type,就可以認識:
record
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
我們比較關心:
這份資料裡有什麼?
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
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
→ 預設比較是不是同一個 Object / Reference
record
→ 預設依資料內容比較
所以兩個 Class Object:
內容一樣
≠
預設就一定相等
而兩個 Record:
資料內容相同
→ 預設可以視為相等
要注意:
這裡說的是預設行為。Class 也可以自行改寫 Equality。
今天不需要深入底層實作。
可以先用這個方向判斷。
比較適合:
物件有明確行為
狀態會持續改變
強調 Object Identity
例如:
ShoppingCart
BankAccount
GamePlayer
這些 Object 不只是保存資料,還可能有:
AddItem()
Deposit()
Withdraw()
Attack()
比較適合:
主要保存資料
主要傳遞資料
希望依內容比較
常搭配較少變動的資料設計
例如:
DTO
API Request
API Response
查詢結果
可以先記:
Class
→ 偏向「物件與行為」
Record
→ 偏向「資料」
這不是絕對規則。
真正還是要看:
這個 Type 在系統裡扮演什麼角色。
這裡要特別注意:
record很適合搭配 Immutable Design,但record本身不代表所有資料一定完全不可變。
所以不要直接記成:
record
=
Immutable
更好的理解是:
record
→ 以資料為中心的 Type
init
→ 限制 Property 建立後重新指定
with
→ 根據原本資料建立修改後的新資料
這幾個概念搭配起來,非常適合:
Immutable Design
Immutability:
Immutable
→ 不可變
可以先理解成:
Object 建立完成後,不隨意直接修改原本資料。
假設一個 Object 到處都能修改:
Method A
→ 修改 Name
Method B
→ 修改 Age
Method C
→ 又修改 Name
程式變大後,很容易產生:
這個值到底在哪裡被改掉?
Immutable Design 的想法是:
原本資料
↓
保留
需要不同資料
↓
建立新的資料
這樣資料變化通常會更容易追蹤。
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
↓
初始化時可以設定
↓
初始化完成後不能直接重新指定
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
沒有被修改。
with 怎麼理解?可以把:
User newUser =
user with
{
Age = 21
};
理解成:
以 user 為基礎
↓
建立一份新的資料
↓
大部分內容保持相同
↓
Age 改成 21
↓
得到 newUser
比較:
Mutable 思維
原本 user
↓
直接修改
↓
Age = 21
和:
Immutable 思維
原本 user
↓
保留
建立 newUser
↓
Age = 21
所以 with 很適合表達:
根據原本資料,建立修改後的新版本。
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
下一階段進入 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
假設:
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
建立:
Order order =
new Order(
"TXF",
20
);
判斷:
Product == TXF
而且
Quantity > 10
符合時輸出:
TXF 大量訂單
根據:
Quantity < 10
→ Small
Quantity < 100
→ Medium
其他
→ Large
使用:
switch expression
建立:
string level
並思考:
如果
Quantity = 5,為什麼結果是Small,而不是Medium?
with建立:
public record User(
string Name,
int Age
);
建立:
Tom
20
接著使用:
with
建立另一份:
Tom
21
確認:
原本 user
→ Tom / 20
新的 newUser
→ Tom / 21
今天學習三個 Modern C# 重要方向。
is
Property Pattern
switch expression
核心:
更清楚地描述「資料符合什麼條件」。
record
→ 適合以資料為中心的 Type
特色之一:
Value Equality
→ 預設依資料內容比較
核心:
更清楚地表示「這是一份資料」。
init
→ 初始化時設定資料
with
→ 建立修改後的新資料
核心:
不直接修改原本資料
↓
需要變化
↓
建立新的資料
要注意:
record
≠
完全 Immutable
而是:
record
+
init
+
with
很適合用來實作 Immutable Design。
整篇最重要的一句話:
Modern C# 的價值不在於語法比較新,而是讓程式碼更清楚地表達條件、資料與變化。