Menu

C# switch 表达式:语法、模式、弃元与 when

C# 8 的 switch 表达式用模式分支代替 case 和 break,把一个值转换成结果。语法,_ 弃元,属性、元组和关系模式,and/or/not,when 条件,穷尽性警告与 SwitchExpressionException,并附有可运行的 C# 7 等价写法。

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

switch 表达式把一个输入值映射为一个结果。它在 C# 8 中加入,取代了一种常见写法:每个 case 都只是返回或赋值的 switch 语句:

// C# 8 and later
string StatusText(int code) => code switch
{
    200 => "OK",
    301 or 302 => "Redirect",   // 'or' needs C# 9
    404 => "Not Found",
    >= 500 => "Server Error",   // relational patterns need C# 9
    _ => "Unknown"
};

从上往下读:switch 前面的值按顺序与每个分支进行比较,第一个模式匹配的分支提供结果。没有 case,没有 break,也没有 default 关键字;_(弃元模式)可以匹配任何值,充当兜底分支。

在 C# 7 中,同样的映射要写成一个小方法里的 switch 语句,每个 case 中都有 return:

输出:

200 OK
302 Redirect
404 Not Found
503 Server Error
418 Unknown

表达式写法更短,主要是因为每个分支只有一行,而且方法体不见了。它还是一个表达式,所以可以出现在任何需要值的地方:参数、插值的花括号、return、字段初始化器。

语法规则

var result = input switch
{
    pattern1 => value1,
    pattern2 when condition => value2,
    _ => fallback
};
  • 先写输入,再写 switch。这与语句形式的顺序相反。
  • 每个分支是 pattern => expression。右边必须是单个表达式;需要更多逻辑时,调用一个方法。
  • 分支之间用逗号分隔。最后一个分支后面允许有逗号。
  • 整个结构是一个表达式,所以使用它的语句要在右花括号后面以 ; 结束。
  • 所有分支必须产生具有共同类型的值。var x = n switch { 0 => "zero", _ => 0 }; 无法编译(CS8506,没有最佳类型)。从 C# 9 起,声明目标类型就能解决:object x = n switch { 0 => "zero", _ => 0 }; 可以编译。
  • 每个分支都必须产生一个值,所以分支不能调用 Console.WriteLine 这样的 void 方法,switch 表达式也不能单独作为一条语句(CS0201)。要执行操作,请使用 switch 语句。
  • 分支可以抛出异常而不产生值:_ => throw new ArgumentOutOfRangeException(nameof(input))。
  • 如果某个分支因为前面的分支已经覆盖了它而永远无法到达,编译器会以错误 CS8510 拒绝它:The pattern is unreachable. It has already been handled by a previous arm of the switch expression or it is impossible to match. 请把具体的分支放在通用的分支前面。

关系模式与逻辑模式(C# 9)

C# 9 把 <、<=、>、>= 加入了模式,还加入了组合符 and、or 和 not。它们让范围表读起来就像规格说明:

// C# 9 and later
string Grade(int score) => score switch
{
    < 0 or > 100 => throw new ArgumentOutOfRangeException(nameof(score)),
    >= 90 => "A",
    >= 80 => "B",
    >= 70 => "C",
    >= 60 => "D",
    _ => "F"
};

string AgeGroup(int age) => age switch
{
    < 13 => "child",
    >= 13 and <= 19 => "teen",
    _ => "adult"
};

bool IsLetter(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';

因为分支是按顺序尝试的,>= 80 只会看到低于 90 的分数。关系模式的右边只能是常量;要与变量比较,请使用 when 条件。范围表在 C# 7 中的等价写法是一串 if:

输出:

95 -> A
83 -> B
71 -> C
64 -> D
12 -> F

not 最常见的用法是 not null:x is not null,或者一个分支 not null => x.Name。

属性模式

属性模式按对象的成员进行匹配:{ Property: pattern, ... }。与 switch 表达式结合使用,可以不用一长串 && 条件就表达业务规则:

// C# 8 and later
decimal ShippingCost(Order order) => order switch
{
    { Total: >= 100m } => 0m,                       // relational inside: C# 9
    { Country: "US", Express: true } => 15m,
    { Country: "US" } => 5m,
    { Express: true } => 30m,
    null => throw new ArgumentNullException(nameof(order)),
    _ => 12m
};

列出的每个属性都必须匹配,这个分支才会匹配。空的属性模式 { } 匹配任何非 null 的值。C# 10 新增了针对嵌套成员的扩展属性模式:用 { Customer.Address.Country: "US" } 代替 { Customer: { Address: { Country: "US" } } }。

元组模式

对元组进行 switch 可以同时匹配多个值,这是表达决策表的简洁方式:

// C# 9 and later (the 'or' pattern)
string Winner(string a, string b) => (a, b) switch
{
    ("rock", "scissors") or ("scissors", "paper") or ("paper", "rock") => "player 1",
    var (x, y) when x == y => "draw",
    _ => "player 2"
};

var (x, y) 把元组解构成两个变量,供 when 条件进行比较。在 C# 7 中,常见的替代做法是把多个值合并成一个键,或者使用一个存放结果的字典:

输出:

player 1
player 2
draw

类型模式与 when

分支可以检查运行时类型并绑定一个变量,when 子句则可以添加模式语法无法表达的任何条件:

// C# 8 and later
double Area(Shape shape) => shape switch
{
    Circle c => Math.PI * c.Radius * c.Radius,
    Rectangle r when r.Width == r.Height => r.Width * r.Width,
    Rectangle r => r.Width * r.Height,
    null => throw new ArgumentNullException(nameof(shape)),
    _ => throw new NotSupportedException(shape.GetType().Name)
};

C# 7 版本在一串 if 中使用 is 类型模式,它以同样的方式绑定变量:

输出:

Circle: 12.57
Rectangle: 13.50

穷尽性与 SwitchExpressionException

switch 表达式必须为每个输入都产生一个值。当编译器发现有输入没有任何分支能匹配时,会发出警告:

warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern '0' is not covered.

这是警告而不是错误,所以代码仍然可以构建。如果运行时出现了未匹配的值,表达式会抛出 System.Runtime.CompilerServices.SwitchExpressionException,其消息末尾是那个未匹配的值(Unmatched value was 3.)。

枚举是一个特例。即使每个具名成员都有对应的分支,枚举变量仍然可以保存任意整数((Size)3),所以编译器会针对“未命名的枚举值”报告 CS8524:

// C# 8 and later
enum Size { Small, Medium, Large }

string Code(Size s) => s switch
{
    Size.Small => "S",
    Size.Medium => "M",
    Size.Large => "L",
    // warning CS8524 without the next arm
    _ => throw new ArgumentOutOfRangeException(nameof(s))
};

添加一个显式的 _ 分支,抛出一个有意义的异常。这样既记录了你的假设,又消除了警告,而且万一出现了错误的值,它给出的错误也比 SwitchExpressionException 更清楚。

用字典替代

当分支都是单纯的常量到值的对应关系,而且这张表可能会变大或来自配置时,Dictionary 在任何 C# 版本中都能完成同样的工作,还可以在运行时构建:

输出:

19.00
11.50
0

未知的国家得到的税率是 0m。decimal 乘积的小数位数等于两个操作数的小数位数之和,所以 100m * 0.19m 输出 19.00,而零税率输出 0。

switch 表达式与 switch 语句对比

switch 语句switch 表达式
从哪个版本开始可用C# 1C# 8
产生值否是
每个分支的语法case X: ... break;X => value,
兜底分支default:_ =>
每个分支包含多条语句是否,调用一个方法
穷尽性警告否是(CS8509)
运行时没有匹配什么都不执行SwitchExpressionException

两种形式接受相同的模式(类型、常量、属性、元组、关系),所以学会一种也就学会了另一种。选择取决于形状:产生一个值用表达式,执行一系列操作用语句。

常见问题

C# 中的 switch 表达式是什么?

switch 表达式在 C# 8 中加入,它会求出一个值:string text = code switch { 200 => "OK", 404 => "Not Found", _ => "Unknown" };。输入写在 switch 关键字前面,每个分支是 pattern => result,分支之间用逗号分隔,并且没有 case、break 或 default 关键字。

C# switch 表达式中的默认分支是什么?

弃元模式 _ 可以匹配任何值,所以把 _ => "Unknown" 放在最后一个分支就相当于 default。如果没有兜底分支,当某些输入没有被覆盖时编译器会发出警告(CS8509),运行时遇到未匹配的值则会抛出 SwitchExpressionException。

如何在 switch 表达式的一个分支中匹配多个值?

使用 C# 9 的 or 模式:"sat" or "sun" => "weekend"。对于范围,用 and 组合关系模式:>= 13 and <= 19 => "teen"。在 C# 8 中,你需要把这些值写成结果相同的多个独立分支。

C# switch 表达式中可以用 when 吗?

可以。分支可以在模式后面加一个条件:Order o when o.Total > 100 => 0m。只有模式匹配且 when 条件为 true 时,这个分支才会匹配;否则继续计算下一个分支。

应该用 switch 表达式还是 switch 语句?

当每个分支只是产生一个值时用表达式,比如把状态映射为标签,或者计算价格。当分支要执行多条语句、有副作用或需要跳出循环时用语句。表达式还会给出语句没有的穷尽性警告。

Coddy 编程语言插图

和 Coddy 一起学编程

开始