Skip to content

⭐ 1.3 布尔值(中)

switch 语句

刚刚那样,通过很多个if-else语句判断1个变量的值的代码实在是太常用了。你能想到它的一些用处吗?

正因为这种模式很常见,C#为我们提供了一种简化的语法——switch语句。刚刚的红绿灯代码用switch语句重写:

int lightStatus;

switch (lightStatus)
{
    case 1:
        Console.WriteLine("停");
        break;
    case 2:
        Console.WriteLine("观察");
        break;
    case 3:
        Console.WriteLine("行");
        break;
    default:
        Console.WriteLine("信号灯故障");
        break;
}

怎么?它看起来和之前的if-else语句有点不太一样?我们逐行看看。

首先,switch (lightStatus)。switch是开关的意思(不是用来玩游戏的啦!)。在这里,这个开关有点类似于挡位开关。什么的挡位?就是紧随的括号括住的变量的挡位。生活中我们在哪里见过这类开关呢?汽车!没错,那就是switch (carGear)。还有电风扇的转速:switch (fanSpeed)

常量模式和关系模式

用花括号{}括住的内容(代码块)就是这个开关的详细设计。

case XX:表示的就是,当开关被拨到XX挡(case)的时候,接下来要干什么。我们以case 1:为例说明。当lightStatus == 1,也就是信号灯状态这个“开关”被拨到1的时候,代表了红灯,所以向控制台输出"停"。这种与一个常量比较是否相等的模式叫做常量模式

除了和一个常量比较相等性以外,比较大于、小于、大于等于、小于等于等关系也是很常用的。这种模式叫做关系模式

Warning

没有判断不等于的关系模式。如果需要匹配不等于关系,可以使用即将出场的逻辑模式

假设某平台对游戏评价分级规则是:

等级 好评率
好评如潮 ≥ 95%
特别好评 80% ~ 94%
多半好评 70% ~ 79%
褒贬不一 40% ~ 69%
多半差评 20% ~ 39%
差评如潮 < 20%

我们用关系模式的switch语句实现上述评价逻辑:

游戏评价
double positivePercent;

switch (positivePercent)
{
    case >= 95:
        Console.WriteLine("好评如潮");
        break;
    case >= 80:
        Console.WriteLine("特别好评");
        break;
    case >= 70:
        Console.WriteLine("多半好评");
        break;
    case >= 40:
        Console.WriteLine("褒贬不一");
        break;
    case >= 20:
        Console.WriteLine("多半差评");
        break;
    case < 20:
        Console.WriteLine("差评如潮");
        break;
}

关系模式下的switch语句工作原理和常量模式很类似,case >= 95实际上是在检测条件positivePercent >= 95。如果匹配上了,就执行Console.WriteLine("好评如潮");。然后,break;标志着switch语句的结束,程序会执行后续代码。如果没匹配上,那就说明好评率是不足95%的,那么程序会继续尝试匹配下一个case,也就是case >= 80。以此类推。

来看一个错误示范吧。在下面这个例子中的switch语句,尝试与变量、不同类型的常量比较:

int lightStatus = 5;
int flag = 0;

switch (lightStatus)
{
    case 1:
        Console.WriteLine("停");
        break;
    case 1.5:
        Console.WriteLine("观察");
        break;
    case flag:
        Console.WriteLine("行");
        break;
}

第9行引发了错误 ❌CS0266:无法将类型“double”隐式转换为“int”。存在一个显式转换(是否缺少强制转换?)。而在游戏评价的例子,positivePercentdouble类型的变量。与其比较的9580等整数常量可以顺利地隐式转为double类型,没有任何问题。

第12行也引发了一个错误 ❌CS9135:应为 'int' 类型的常量值。

总结一下,无论是关系模式还是常量模式,我们都是把switch (expression)里的expression和一个相同类型(或者可以隐式转换为相同类型)的常量做比较。在常量模式下,进行相等关系的判断;而在关系模式下,可以比较大小。

贯穿

但……为什么每个case的末尾都有个break;

试试把case 1里的break;删除,立刻引发了错误 ❌CS0163:控制不能从一个 case 标签(“case 1:”)贯穿到另一个 case 标签。咦?什么是“贯穿”?

如果在一个case的末尾加上break;,当switch语句匹配到这个case以后,执行完case中的代码就会忽略这个switch语句内剩下的代码。也就是说,break;起到结束整个switch语句的作用。那不加上break呢?执行完这个case后,还会继续检查剩余的其他case,这就是“贯穿”。

什么?还是不懂?好吧。我们假设switch是一个办事大厅,每个case就是一个办事窗口。一般人都是去对应的窗口办完事就离开办事大厅,这就是break;的作用。有的人在一个窗口办完事,没人请他离开,他就还会去其他窗口,这就是不写break;的后果。

为什么C#禁止“贯穿”呢?这是因为有C++在这方面“坑人”的前车之鉴。在C++里,在窗口办完业务的人,如果你不用break;请他离开大厅,他就会立刻化身不良少年,去接下来的窗口挨个骚扰,直到有窗口用break;叫他滚蛋,或者他骚扰完所有窗口为止。编译器就在一边看着整个过程发生,而不发出任何警告或错误提示。

好吧。所以,在每个case的末尾写上break;还是很有必要的,包括default分支。噢!忘了说了,我们既可以把case看成switch办事大厅里的不同窗口,也可以把它们看成switch路口的不同岔路,每一条岔路就叫一个“分支”。假如所有case都匹配不上,就会执行默认分支,也就是default分支。可以想到,假如所有case分支都匹配不上,那一定是发生了一些意料之外的事情。

回到我们的红绿灯代码。我们设计了case 1case 2case 3分别代表红灯、黄灯和绿灯3种意料之中的状态。假如它们都不能匹配上,说明这时lightStatus可能为4-100或者其他乱七八糟的状态,这时就会匹配上default分支,输出"信号灯故障",完美!

Tip

从这个案例也能发现,default分支经常被用于处理异常情况。

Note

从游戏评价的那个案例可以看出,switch语句里不是必须要有default分支。尽管如此,还是推荐加上一个default分支来为各种意料之外的边缘情况兜底。

在C#中只有一种情况允许贯穿,请看下面的代码:

int lightStatus;

switch (lightStatus)
{
    case 1:
        Console.WriteLine("停");
        break;
    case 3:
        Console.WriteLine("行");
        break;
    case 2:
        Console.WriteLine("观察");
        break;
    default:
        Console.WriteLine("观察");
        break;
}

发现case 2这个分支和它的下一个分支执行的操作是一样的。本着DRY原则,C#允许我们把多个操作重复的分支“合并”一下:

int lightStatus;

switch (lightStatus)
{
    case 1:
        Console.WriteLine("停");
        break;
    case 3:
        Console.WriteLine("行");
        break;
    case 2:
    default:
        Console.WriteLine("观察");
        break;
}

就像上面这样,我们把case 2分支的内容都删掉了。这时,如果lightStatus匹配到case 2,就会贯穿到下一个分支,也就是default分支,执行Console.WriteLine("观察");。这是符合我们预期的行为。

测验时间

对于被“合并”的分支来说,各个分支条件实质上构成了哪种逻辑关系?

查看答案

逻辑“或”的关系。比如在上一个例子中,case 2分支和default分支合并后,相当于:满足lightStatus == 2 或者 没有匹配上所有case(即default的触发条件),才执行Console.WriteLine("观察");

当然,在少数情况下,执行完一个分支以后,如果确实需要接着执行另一个分支,可以使用goto语句导航到别的分支,替换原来的break;语句。比如goto case 1;goto default;这样。

Case 的顺序很重要

switch语句会从上到下依次检查各个case的条件是否匹配。在常量模式中,每个case之间一般是互斥的,它们之间的排列顺序可以调整。但是,从代码可读性角度看,最好还是按照一定的顺序排列。在我们的红绿灯案例中,我们可以按照lightStatus的状态码,按case 1、case 2、case 3的顺序排列。对于有更多case的switch来说,当需要对某一个case进行维护时,按顺序排列能帮助你快速定位代码。

Note

常量模式下,如果你的case之间不互斥,比如设了2个case 1,会引发错误 ❌CS8120:该 switch case 不可访问。它已由上一 case 处理或无法匹配。

在常量模式以外,各case的条件可能有交集。此时,case的顺序非常重要!不仅顺序不能随意调换,case的判断条件也需要非常仔细地设计。

Case 守卫

前面,我们设计了一个游戏评价系统。这个系统上线以后,有人反馈了意见——假如某个游戏虽然质量过关,但知名度很低,只有区区几条好评,没有差评。而我们的系统判断这款游戏好评率100%,给了“好评如潮”的评价,“如潮”在哪?

看来,我们不得不修改评价标准,把评论数也纳入考核:

等级 好评率 最低评测数
好评如潮 ≥ 95% ≥ 500 条
特别好评 ≥ 95% < 500 条
特别好评 80% ~ 94% 无要求
多半好评 70% ~ 79% 无要求
褒贬不一 40% ~ 69% 无要求
多半差评 20% ~ 39% 无要求
特别差评 < 20% < 500 条
差评如潮 < 20% ≥ 500 条

解释一下关键的修改。我们给“好评如潮”和“差评如潮”两个评价等级加上了评论数必须达到500条的要求。如果评论数不够的话,即使好评率达到了95%也只能评为“特别好评”;即使好评率低于20%也只能评为增设的“特别差评”等级。

我们先分析一下好评如潮这一档。它在满足好评率不小于95%的同时,还要满足评论数不小于500条这一附加条件。因此,我们可以沿用之前的switch语句,在这个case里设置一个if语句:

double positivePercent;
int totalReviews;

switch (positivePercent)
{
    case >= 95:
        if (totalReviews >= 500)
        {
            Console.WriteLine("好评如潮");
        }
        break;
// (以下省略)

完美解决!假如后续还需要加入更多评价规则,我们只需要修改if语句的条件就好了。

因为这种做法很常用,C#为我们提供了一种简化的写法:

switch (positivePercent)
{
    case >= 95 when totalReviews >= 500:
        Console.WriteLine("好评如潮");
        break;
// (以下省略)

在原来的case >= 95后面加一个when,然后加上原来的if语句的条件,从而省略if。

totalReviews >= 500这样,给case加上的附加条件叫做case守卫(case guard)。想要进入“好评如潮”这个case分支,不单要满足条件positivePercent >= 95,还得通过case守卫的考验。

题外话

switch是我开,此case是我栽。要想从此过,满足case guard!

语法糖

改写成when子句以后,代码的形式变简单了,但是功能和原来的case里面套if没区别。这种为便利程序员而设置的简化语法叫做“语法糖”。

测验时间又到了

when 子句实现完整的改进版游戏评价系统。

参考答案
double positivePercent;
int totalReviews;

switch (positivePercent)
{
    case >= 95 when totalReviews >= 500:
        Console.WriteLine("好评如潮");
        break;
    case >= 80:
        Console.WriteLine("特别好评");
        break;
    case >= 70:
        Console.WriteLine("多半好评");
        break;
    case >= 40:
        Console.WriteLine("褒贬不一");
        break;
    case >= 20:
        Console.WriteLine("多半差评");
        break;
    case < 20 when totalReviews >= 500:
        Console.WriteLine("差评如潮");
        break;
    case < 20:
        Console.WriteLine("特别差评");
        break;
}

注意到,case >= 95 when totalReviews < 500完全包含在case >= 80中,因此前者被省略了。

同时注意到,特别差评和差评如潮对好评率的要求是一致的,二者的case分支的关系模式也都是< 20。由于switch语句是从上到下依次检查case的,我们把排在前面的case分支添加判断评测数是否达到500的when子句,用于差评如潮的判定。这样以来,对于排在后面的case分支,匹配上的前提是不满足case < 20 when totalReviews >= 500:的同时又满足case < 20:,言下之意就是好评率低于20%且评测数不足500。

请仔细体会各个case之间为什么是按照这样排序。如果改变顺序,需要如何修改模式?

逻辑模式

我们的改进版游戏评价系统推出后,出现了游戏开发者花钱买好评和给竞争对手打差评的现象。所以,我们需要利用最近15天的评论数和根据这些评论计算出的好评率,排查评论数≥500条,且好评率≥90%和好评率≤10%的游戏是否存在刷评论行为。

double recentPositivePercent;
int recentTotalReviews;

switch (recentPositivePercent)
{
    case >= 90 when recentTotalReviews >= 500:
        Console.WriteLine("需要关注");
        break;
    case <= 10 when recentTotalReviews >= 500:
        Console.WriteLine("需要关注");
        break;
}

两个case执行的操作一样,它们可以合并:

switch (recentPositivePercent)
{
    case >= 90 when recentTotalReviews >= 500:
    case <= 10 when recentTotalReviews >= 500:
        Console.WriteLine("需要关注");
        break;
}

在讲贯穿的末尾,测验时间提到“合并”的case之间是逻辑的关系。C#提供了一个关键词or,用来帮助我们把两个或关系的case真正地合并到一起:

switch (recentPositivePercent)
{
    case >= 90 or <= 10 when recentTotalReviews >= 500:
        Console.WriteLine("需要关注");
        break;
}

同样表达逻辑或关系,or和逻辑运算符||之间还是很好区分的。关键看它们将什么东西连接在一起。逻辑运算符||的两边一定会是最终会得到true或者false的东西。像布尔表达式:

rate > 5 || rate < 9
a == 2 || b <= 3

而逻辑模式or的两边是模式,也就是我们前面写了那么多的case分支中,关键词case后面跟着的东西。比如关系模式:

>=90 or <=10
>3.2 or <7.6

或者常量模式:

2 or 3
3.2 or 7.6

现在,回过头来看case >= 90 or <= 10 when recentTotalReviews >= 500:到底在干嘛。在这个case分支中,首先用逻辑模式or把两个关系模式>= 90<= 10联合起来,形成“或”的关系。如果匹配上了这个联合模式,再进行when子句的判断。

when子句的优先级

作为case分支的守卫,when子句永远是在其他模式都匹配上之后,才进行最后把关式的条件检验。

你可以把它理解为case分支内嵌套的if语句。

同理,我们还可以用and来连接两个关系模式,实现逻辑“与”,比如case > 4 and < 6。但是,and不会用在连接常量模式。无论是像case 2 and 3这样,总是得到必定不能匹配的模式;还是像case 6 and 6这样重复的模式,都完全没有意义。

我们还能用not来对模式取非。case not > 10就相当于case <= 10case not 2相当于……好吧,没有表示不等于的关系模式,它用于匹配所有不是2的情况。

讲逻辑总是很烧脑。所以伸个懒腰,放松一下吧。