模式匹配用一种“形状”来检测一个值:“它是 Circle 吗?”“它是 null 吗?”“它的 Status 是不是“paid”,Total 是不是超过 100?”检测成功时,模式还能把值的某些部分取出来放进新的变量。它在 C# 7 中以 is 运算符起步,此后每个版本都在扩展,所以本页会注明每种形式对应的版本。
is 类型模式(C# 7)
在 C# 7 之前,检查类型并使用它需要两步:as 加一次空值检查,或者 is 加一次强制转换。类型模式一步就能完成这两件事,并给你一个类型确定的变量:
输出:
card ending 4242: 49.90
transfer from PT50...: 1200
no payment
other payment: 5
只有当 p 是一个非 null 的 CardPayment(或其派生类)时,p is CardPayment card 才为真,此时 card 以正确的静态类型持有它。null 值永远不会匹配类型模式,所以那个为 null 的付款一路落到了空值检查那里。
作用域与明确赋值
模式变量存在于外层代码块中,但编译器只允许你在确定匹配已经成功的地方读取它。这让“保护子句”的写法成为可能:先提前退出,之后再使用这个变量:
输出:
19.99
42 is over 40
price must be text
boxed is int n && n > 40 展示了另一种常见写法:变量可以在 && 的右边使用,因为只有匹配成功时右边才会执行。在 || 的右边它没有被赋值,编译器会指出这一点(CS0165)。
模式还能拆箱:对于装箱的 int,boxed is int n 会成功,并且不需要可能抛出异常的强制转换就能得到值。对于可空值类型,maybe is int n 恰好在 maybe 有值时成功。
常量模式与 null 模式
常量也是一种模式。x is null 是最常用的一个,C# 7 也允许 x is 0、status is "paid" 或枚举成员:
if (customer is null) return; // null check that ignores any == overload
if (count is 0) Console.WriteLine("empty");
if (level is LogLevel.Error) Alert();
许多团队更喜欢 is null 而不是 == null,因为它不会被该类型上的运算符重载所改变。C# 9 增加了否定形式 is not null。
switch 语句中的模式(C# 7)
同样的类型模式也可以作为 case 标签,when 子句则再加上一个条件。各个 case 从上到下依次尝试,所以要按从具体到一般的顺序排列:
static string Describe(object o)
{
switch (o)
{
case null: return "nothing";
case int n when n < 0: return "negative number";
case int n: return $"number {n}";
case string s when s.Length == 0: return "empty text";
case string s: return $"text of {s.Length} chars";
case IEnumerable<int> list: return "a sequence of numbers";
default: return o.GetType().Name;
}
}
Describe(-4); // negative number
Describe(""); // empty text
Describe(new List<int>()); // a sequence of numbers
Describe(2.5); // Double
编译器会检查顺序:因为前面的 case 已经覆盖而永远不可能匹配的 case 会产生错误 CS8120。无论 default 写在哪里,它总是最后才被求值。用 if/is 链写出的同样逻辑(就像上面付款的例子)在 C# 7 及以后的每个版本中都能运行。
switch 表达式(C# 8)
switch 表达式是“从几种情况中算出一个值”的紧凑写法。先写值,然后是 switch,接着是若干个用逗号分隔的 pattern => result 分支,用 _ 兜底:
public abstract record Shape;
public record Circle(double Radius) : Shape;
public record Rectangle(double Width, double Height) : Shape;
static double Area(Shape shape) => shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle { Width: var w, Height: var h } => w * h,
_ => throw new ArgumentException("Unknown shape", nameof(shape)),
};
Area(new Rectangle(3, 4)); // 12
如果运行时没有任何分支匹配,它会抛出 SwitchExpressionException;当编译器能看出有些输入没有被处理时,它会发出警告(CS8509)。switch 表达式页面单独介绍了它的语法;本页其余部分讲的是你可以放进各个分支的模式。
属性模式(C# 8,C# 10 中扩展)
属性模式在花括号中用嵌套的模式来匹配对象的属性。对象本身也必须不为 null,所以单独的 { } 表示“不为 null”:
public record Address(string City, string Country);
public record Order(decimal Total, string Status, Address ShipTo, int Items);
static string Shipping(Order order) => order switch
{
{ Status: "cancelled" } => "no shipment",
{ ShipTo.Country: "PT", Total: >= 50m } => "free, domestic", // C# 10 dotted form
{ ShipTo.Country: "PT" } => "4.90, domestic",
{ Total: > 200m } => "free, international",
_ => "12.00, international",
};
Shipping(new Order(60m, "paid", new Address("Porto", "PT"), 2)); // free, domestic
Shipping(new Order(260m, "paid", new Address("Lyon", "FR"), 2)); // free, international
在 C# 8 和 9 中必须使用嵌套形式:{ ShipTo: { Country: "PT" } }。C# 10 允许使用上面那种带点号的简写。属性模式也可以和 is 一起使用,让多条件检查读起来就像一段描述:if (order is { Status: "paid", Items: > 0 })。
关系模式与逻辑模式(C# 9)
C# 9 增加了比较(<、<=、>、>=)以及组合符 and、or 和 not,它们能把范围写成模式:
static string Grade(int score) => score switch
{
< 0 or > 100 => "invalid",
>= 90 => "A",
>= 75 and < 90 => "B",
>= 50 => "C",
_ => "F",
};
Grade(82); // B
Grade(101); // invalid
if (input is not null and not "") { ... }
if (c is >= 'a' and <= 'z' or >= 'A' and <= 'Z') { ... } // and binds tighter than or
在 C# 7 中,等价的写法是 if/else if 链,或者带 when 子句的 switch(case int s when s >= 90:)。
元组模式与位置模式(C# 8)
要同时对多个值进行 switch,可以对一个元组进行 switch:
static string Quadrant(int x, int y) => (x, y) switch
{
(0, 0) => "origin",
(> 0, > 0) => "I",
(< 0, > 0) => "II",
(< 0, < 0) => "III",
(> 0, < 0) => "IV",
_ => "on an axis",
};
Quadrant(-2, 5); // II
位置模式对任何带有 Deconstruct 方法的类型(包括记录类型)做同样的事:case Point(0, 0): 或 p is Rectangle(var w, var h) && w == h。
列表模式(C# 11)
列表模式按元素来匹配数组和列表。.. 匹配任意数量的元素(一个“切片”),每个元素位置都可以放任何其他模式:
static string Route(string[] args) => args switch
{
[] => "help",
["add", var item] => $"add {item}",
["remove", var item, ..] => $"remove {item}",
[var cmd, ..] => $"unknown command {cmd}",
};
Route(new[] { "add", "milk" }); // add milk
Route(new[] { "remove", "milk", "now" }); // remove milk
int[] nums = { 1, 2, 3, 4 };
if (nums is [1, .., var last]) Console.WriteLine(last); // 4
各种模式由哪个版本加入
| 模式 | 示例 | 版本 |
|---|---|---|
| 带变量的类型模式 | o is Circle c | C# 7.0 |
| 常量模式 | x is null、n is 0 | C# 7.0 |
var 模式 | x is var v | C# 7.0 |
case 标签中的模式、when 条件 | case int n when n < 0: | C# 7.0 |
switch 表达式、弃元 _ | x switch { ... } | C# 8 |
| 属性模式 | { Status: "paid" } | C# 8 |
| 元组模式 | (x, y) switch { (0, 0) => ... } | C# 8 |
| 位置模式 | Point(0, 0) | C# 8 |
| 关系模式 | > 100、<= 0 | C# 9 |
| 逻辑模式 | not null、>= 1 and <= 5、'a' or 'b' | C# 9 |
| 单纯的类型模式 | case Circle:、Circle => ... | C# 9 |
| 扩展属性模式 | { ShipTo.City: "Porto" } | C# 10 |
| 列表模式与切片 | [first, .., last] | C# 11 |
常见错误
- 按从一般到具体的顺序排列 case。 把
case Payment p:放在case CardPayment c:前面,会让后者永远无法到达(CS8120)。具体的放在前面。 - 以为类型模式会匹配 null。
null is string s是 false。用单独的 case 或is null处理null。 - 在匹配可能失败的地方读取模式变量。 在
a is int n || ...之后,n没有被赋值(CS0165)。 - 用冗长的类型 switch 代替虚方法。 如果每新增一个子类都要在五个地方修改同一个 switch,那么这些行为就应该放到类层次结构里。模式最适合处理来自外部的数据(JSON 结构、消息、多个输入组成的元组),以及你能掌控的封闭类型集合。
- 忘了给 switch 表达式加兜底分支。 未匹配的输入会在运行时抛出异常;留意警告 CS8509。
常见问题
C# 中的模式匹配是什么?
模式匹配用一种“形状”来检测一个值,匹配成功时,还能在同一步中把值的某些部分取出来放进变量。if (shape is Circle c) 检测类型并给你一个类型确定的 c;order is { Status: "paid", Total: > 100 } 检测属性。模式可以出现在 is 表达式、switch 语句和 switch 表达式中。
C# 中如何配合变量使用 is?
if (obj is Customer c) { ... } 检查 obj 是否是 Customer(且不为 null),并在 if 内部把它赋给类型为 Customer 的 c。它取代了更老的 var c = obj as Customer; if (c != null) 写法。只有在确定检测结果为真的地方,这个变量才算明确赋值,所以 if (!(obj is Customer c)) return; 之后,c 在 if 后面仍然可以使用。
C# 中 is null 和 == null 有什么区别?
x is null 总是检查引用本身。x == null 会调用该类型的 == 运算符(如果它重载了的话),通常结果相同,但不能保证。相反的判断可以写成 x != null、!(x is null),或者从 C# 9 起写成 x is not null。
C# 中带 when 的 switch 模式匹配是怎样工作的?
case 标签可以包含一个模式,再加一个 when 条件:case Order o when o.Total > 100:。只有当模式匹配且条件为真时,这个 case 才算匹配;各个 case 从上到下依次尝试,所以要把更具体的放在前面。当一个 case 因为前面的 case 已经覆盖了它而永远不可能被执行时,编译器会报告 CS8120。
各种模式分别是在哪个 C# 版本加入的?
C# 7.0:is 和 case 中的类型模式、常量模式和 var 模式,以及 when。C# 8:switch 表达式,属性模式、元组模式和位置模式。C# 9:关系模式(> 5)、逻辑模式(and、or、not)和单纯的类型模式。C# 10:扩展属性模式({ Address.City: "Porto" })。C# 11:列表模式([1, .., var last])。