引言
隨著C#
不斷發展,"模式匹配"(Pattern Matching)已經成為讓代碼更加友好、可讀和強大的核心特性。從 C#
7.0 初次引入,到 C#
11的能力擴展,模式匹配為處理類型判斷、屬性解構、集合匹配等提供了簡潔、高效且類型安全的表達方式。它不僅能讓 if/switch 等控制結構變得“聲明式”,還能帶來性能提升。在這篇文章里,我們將深入剖析 C 的所有模式匹配語法和用法,追蹤其演變,講清一些容易混淆和誤用的地方,讓大家能了解模式匹配本質。
1. 什么是模式匹配
模式匹配本質上是一種表達式判定工具:用以檢查一個對象是否與某種“模式”相吻合,如果吻合,還允許對其分解、綁定成員變量。這可以是類型檢查、常量判斷、屬性結構匹配等。
傳統C#寫法:
if (person != null && person.Age >= 18 && person is Employee
&& ((Employee)person).YearsAtCompany > 5) {
// 處理邏輯
}
模式匹配寫法:
if (person is Employee { Age: >= 18, YearsAtCompany: > 5 }) {
// 處理邏輯
}
更簡潔、可讀、類型安全,不需要重復顯式強制轉型。
2. C#7.0 初代模式匹配特性
2.1 Null 模式與常量模式
C#7.0 首先支持了用is
直接與null以及常量對比:
if (obj is null) Console.WriteLine("對象為null");
if (d is Math.PI) Console.WriteLine("d 是圓周率");
if (str is "test") Console.WriteLine("str內容等于test");
注意:常量匹配僅支持編譯期常量,比如數字、字符串、bool、enum、const字段和null。不能直接用非const變量或表達式。
這個限制為何存在?
- 編譯器要保證 switch 和 if 全覆蓋檢查,如果允許變量參與,則難推導 exhaustiveness(全覆蓋)。
- 也可避免不可預期的副作用與邏輯混亂(比如變量運行期改變等)。
2.2 類型模式與變量捕獲
類型模式允許 you not only 判斷類型,還能直接捕獲變量:
if (shape is Circle circle1)
Console.WriteLine($"圓的半徑為 {circle1.Radius}");
甚至可以鏈式判斷:
if (shape is Rectangle r && r.Width == r.Height)
Console.WriteLine("正方形");
非常適用于臨時變量創建、減少強制類型轉換(顯式as/cast)代碼雜音。
is
vs as
?
is
結合新模式后可以直接安全聲明變量,無需后續 null 檢查。as
后還得寫 if (x != null)
。
2.3 Discard 與 var 模式
- Discard(
_
):匹配但忽略,用于 switch 的 default 分支或類型“只是判斷,不關心值”。 - var:總是匹配成功,并引出變量。通常用于解構場景,比如 switch/case 匹配對象成員。
if (obj is var o)
Console.WriteLine(o); // 不管 obj 是否為null,o 指向原始值(即使null)
?? 這里容易誤用!
不要用 var 跳過 null 檢查。因為這樣會把 null 值“吞掉”,造成 NullReferenceError 隱患。
if (p is var _)
{ // 這里仍可能是 null
Console.WriteLine(p.Length); // NullReferenceException
}
2.4 switch 語句的模式匹配
傳統 switch 只能匹配簡單的枚舉或數字等,C#7.0 后支持以下寫法:
switch (seq) {
case Array a: return a.Length;
case ICollection<T> c: return c.Count;
case IEnumerable<T> _: return seq.Count();
default: return 0;
}
- 某類型滿足條件即分支命中
- Discard
case IEnumerable<T> _:
- default處理剩余情況
switch 的 when 子句
when
只能用于 switch:
case Array a when a.Length < 10: return a.Length;
無法用于 if!
3. C#8.0: Switch表達式、屬性、位置、元組模式
3.1 表達式 Switch
C#8 引入 switch-表達式,極大提升 switch 可讀性和函數式編程體驗:
string whatShape = shape switch {
Circle c => $"circle radius: {c.Radius}",
Rectangle _ => "rectangle",
_ => "null or unknown"
};
=>
右邊直接返回值,非常適合表達式體成員:
public static string Describe(this Shape s) => s switch { ... };
3.2 屬性模式 (Property Patterns)
可直接針對屬性表達式判定
if (shape is Circle { Radius: 1.0 })
Console.WriteLine("單位圓");
whatShape = shape switch {
Rectangle { Width: 10, Height: 5 } => "10x5 矩形",
Rectangle { Width: var x, Height: var y } => $"rect: {x}x{y}",
{ } => "非null",
_ => "null"
};
重點說明:
{ }
代表對象非null。{ Width: 10, Height: 5 }
同時要求2個屬性等于指定值。{ Width: var x, Height: >5 }
可以對屬性用關系操作和抽取變量。- 若對象有
Deconstruct
方法,可用位置/元組模式更優雅。
3.3 位置模式(Positional Patterns)
利用 Deconstruct 抽取變量:
public void Deconstruct(out double w, out double h) => (w, h) = (Width, Height);
shape switch {
Rectangle(var x, var y) => $"矩形尺寸:{x}x{y}",
...
}
3.4 元組模式
處理多個參數的模式組合:
(c1, c2, c3) switch {
(Color.Blue, Color.White, Color.Red) => "France",
(Color.Green, Color.White, Color.Red) => "Italy",
_ => "Unknown"
};
- 有效解決“復雜組合條件冗長”的老大難。
- 更直觀表達多變量匹配,不需層層嵌套。
4. C#9.0: 組合、括號及關系模式
4.1 組合模式(and/or/not)
組合條件極其強大:
c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '.' or ','
等價于:
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '.' || c == ','
not 語法
if (obj is not null) { ... }
比傳統 !(obj is null)
更直觀。
4.2 關系模式(Relational Patterns)
直接用 >, <, >=, <= 表達區間
rate = monthlyIncome switch {
>=0 and <1000 => 0,
<5000 => 10,
_ => 20
};
結合屬性/類型模式:
shape switch {
Circle { Radius: >1 and <=10 } => "較合理圓",
Circle { Radius: >10 } => "過大圓",
Rectangle => "矩形",
_ => "不匹配"
};
4.3 類型模式精簡
C#9 類型模式無需 underscore,eg:Rectangle
直接寫。
5. C#10.0: 擴展屬性模式
支持嵌套點表達式:
之前版本:
x is Person { FirstName: { Length: <=5 } }
C#10:
x is Person { FirstName.Length: <=5 }
讓屬性鏈判定更加直觀簡潔。
6. C#11.0: 列表與切片模式
6.1 列表模式
讓數組/集合結構模式判定成為可能:
if (arr is [1, 2, 3])
// 匹配長度為3,內容依次為1,2,3
[..]
: 切片模式,等價于“0個或多個元素”_
: 忽略一個元素(discard)
[_, >0, ..] // 至少2個元素,第二個>0
[.., <=0, _] // 至少2個元素,倒數第二個<=0且不關心最后一個
注意:切片只能出現一次。
6.2 列表遞歸
可以做嵌套匹配:
bool EndsWithSingleIntList(List<List<int>> lists) => lists is [.., [_]];
// 至少一個元素,且最后一個是單元素list
6.3 支持的結構
只要類型有 Length
/Count
屬性和支持 [index]
則可用列表模式,如 string、數組、自定義集合(只需這兩個成員)。
例如處理國別碼:
switch (s) {
case [char c0, char c1]: // 即 s.Length==2
...
}
7. 模式匹配背后的實現與性能分析
7.1 不是“魔法”,而是IL優化
許多人以為 pattern matching 是一種基于反射或者表達式樹的黑魔法。實際上,編譯器常常能把很多 pattern 匹配轉為高效的“順序判斷”或“跳表”等機器碼。
例如:
c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '.' or ','
編譯后 Equals:
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '.' || c == ','
對于一組常量匹配:
statusCode is 0x1000 or 0x1001 or 0x1002 or 0x2000 or 0x2001 or 0x3000
編譯器會變換成:
return ((uint)(statusCode - 4096) <= 2u || (uint)(statusCode - 8192) <= 1u || statusCode == 12288);
即用范圍短路、減少 equals 判斷次數。
?? 建議:
對于性能敏感代碼,請結合 Benchmark.NET 做實測對比。有時手寫的判定和 pattern matching 效果類似,甚至后者更優。
8. 常見誤區與最佳實踐
8.1 不要用 pattern 替代多態
最容易見的誤用是用模式匹配處理繼承結構的多態行為:
// xxx
public static double Area(this Shape shape) => shape switch {
Circle c => c.Radius * c.Radius * Math.PI,
Rectangle r => r.Width * r.Height
};
事實上,應該優先用抽象基類/接口的虛方法或屬性:
abstract class Shape { public abstract double Area {get;} }
class Circle : Shape {
public double Radius { get; set; }
public override double Area => Radius * Radius * Math.PI;
}
為什么?
- 可擴展性強(新子類無需修改 Area 邏輯,符合開閉原則)。
- 虛調用比類型判定快。
- 更少維護負擔。
8.2 Constant-Only 限制
只有編譯期常量才能用于模式匹配,如果需要支持運行時變量,需采用傳統判定。
8.3 避免濫用 var/discard
- 不要用
is var x
跳過 null 檢查。 - discard
_
只適合確實不關心值的場景,用后不要嘗試訪問。
8.4 List/Slice 模式性能
使用 [..var arr, x]
這種 slice+變量捕獲,編譯器可能分配新數組,造成性能下降。大數據集合應謹慎。
9. 小結與展望
Pattern Matching 是現代 C# 代碼的“瑞士軍刀”,能極大提升 if/else、switch/case 類代碼的簡潔性、表達力和類型安全性,在 switch 表達式等場景下優勢更加明顯。它不僅代碼層面更美觀,也能為某些條件分支提供更優指令級性能。然而,模式匹配并非多態(polymorphism)、虛方法的替代品!業務邏輯與類型分發仍建議用面向對象原則解決。
?轉自https://www.cnblogs.com/InCerry/p/-/introduce-cs-pattern-match
該文章在 2025/6/9 9:15:57 編輯過