Menu

C# 模式匹配:is、switch 模式、属性模式与列表模式

C# 中模式匹配的工作方式:is 运算符配合类型模式和常量模式,带 case 模式和 when 的 switch 语句,switch 表达式,属性、元组和位置模式,关系模式与逻辑模式(and、or、not),列表模式,以及每种模式是在哪个 C# 版本加入的。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

模式匹配用一种“形状”来检测一个值:“它是 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 cC# 7.0
常量模式x is null、n is 0C# 7.0
var 模式x is var vC# 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、<= 0C# 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])。

Coddy 编程语言插图

和 Coddy 一起学编程

开始