⭐ 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语句,尝试与变量、不同类型的常量比较:
第9行引发了错误 ❌CS0266:无法将类型“double”隐式转换为“int”。存在一个显式转换(是否缺少强制转换?)。而在游戏评价的例子,positivePercent是double类型的变量。与其比较的95、80等整数常量可以顺利地隐式转为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 1、case 2、case 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的东西。像布尔表达式:
而逻辑模式or的两边是模式,也就是我们前面写了那么多的case分支中,关键词case后面跟着的东西。比如关系模式:
或者常量模式:
现在,回过头来看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 <= 10。case not 2相当于……好吧,没有表示不等于的关系模式,它用于匹配所有不是2的情况。
讲逻辑总是很烧脑。所以伸个懒腰,放松一下吧。