Skip to content

⭐ 1.3 布尔值(下)

switch 表达式

搞定switch语句后,我们再看它的兄弟——switch表达式。

表达式和语句

表达式(expression)和语句(statement)的主要区别就是,表达式会产生一个值(计算结果),而语句执行某种操作。一种简单粗暴的检验方法:可以用来赋值的是表达式。

让我们趁机复习一下之前学习的一些语句和表达式吧。比如加法表达式、赋值表达式、自增表达式、逻辑运算表达式等等:

int a = 2 + 3;          // 1. 加法表达式

int b;
int c = b = a;          // 2. 赋值表达式

b = a++;                // 3. 自增表达式

bool d = true && false; // 4. 逻辑运算表达式

把这些表达式用在赋值语句中,没有引发错误。很好!接着看看if语句:

int a = if (true) { 3 };

看起来引发了好几个错误呢。这说明if确实是个语句——它执行一定的操作,但是不会返回值。这点和Rust不一样,如果你学过Rust的话,小心别混淆了。

同样的,赋值语句也不能用于赋值语句中(什么俄罗斯套娃):

int a;
int b = a = 5;; // 其中的a = 5;是赋值语句

现在,我们可以推测一下switch表达式和switch语句的不同之处。switch语句应该更擅长根据条件执行一些操作,而switch表达式的长处应该在根据条件进行计算并返回一个结果,这个结果可以用在赋值语句中。

switch 表达式的语法

回忆一下switch语句,假如我们要围绕一个值a进行一系列的判断,我们把a放进switch后边的括号里面,对吧:

switch (a)
{
    // do something...
}

但是,在switch表达式中,却是这样写的:

a switch
{
    // do something...
}

a放到了switch的前面,而且没有括号耶。这是我们发现的第一个不同。

再看看分支的写法有没有什么区别吧。在switch语句中记得是这样:

    case pattern:
        // do something...
        break;
    default:
        // do something...
        break;

其中的pattern可以是常量模式、关系模式和逻辑模式。

switch表达式的分支写法是:

    pattern => expression,

噢!这个差别可有点大。研究一下,左边的pattern还是我们熟悉的那几种跟在case后面的模式。模式后面也依然可以加一个when子句进行进一步的筛选。

中间的符号要注意看清楚了,它是“等于大于” => ,而不是“大于等于” >= !这个符号不是用来判断大小关系的逻辑运算符。你可以暂且把它看成一个指向右边的“箭头”(→)。

右边的expression是一个表达式,也就是会产生一个结果的玩意儿。等等,switch表达式不就是一个表达式吗?难道表达式里面还可以放表达式?正是!

现在我们把这3个部分组合起来看,它其实就是一块“指路牌”。指路标牌你见过吗?在街道上、公路上都有对不对?比如“北京 → 500km”、“中山公园 → 300m”。现在有一列路牌,如果我想知道去北京还要走多远,就在这堆标牌中找到写着“北京”的那一块牌子,然后看箭头→指着的路程——500km,完成!

所以switch表达式的分支就是想说,先用a按顺序跟各个分支的pattern依次进行匹配,如果匹配上了,就计算这个分支的“箭头” => 指着的表达式expression,把计算的结果作为switch表达式整体的结果。

注意逗号

switch表达式的一个分支可不是一条完整的语句,记得末尾用逗号 , 而不是分号!

因为花括号{}括住的玩意不是语句,所以这不算代码块。它就叫做“switch表达式体”。(表达式的身体)

不支持贯穿

不像switch语句,switch表达式不支持贯穿,也不支持切换到别的分支,所以也无需使用break;或者goto语句。

相对于switch语句,switch表达式不用写case,语法看起来更清爽简洁。但是,后者的分支只能计算表达式,想要执行复杂的操作的话,还得靠前者。

表达式臂

switch表达式的分支也叫表达式臂(expression arm)。

弃元模式

我们还没说switch表达式的默认分支呢。它的写法会是这样吗?

    default => expression,

不,defaultcase一样,都是关键词,不是模式。默认分支真正的写法是:

    _ => expression,

一个下划线 _ 是什么模式?答案是——弃元模式

在switch表达式的分支中,当其他所有分支的模式都不能匹配时,就会匹配上弃元模式。听起来和switch语句的default分支很像嘛。

switch语句的default分支是可有可无的。没有default分支时,如果没能匹配上任何分支,那就什么也不做就是了。但是作为表达式,switch表达式一定要能得到一个结果,所以编译器会自动帮你检查你设计的分支能不能覆盖所有情况。下面这样的写法是覆盖了所有情况的:

int lightStatus = 3;

int sign = lightStatus switch
{
    > 0 => 1,
    0 => 0,
    < 0 => -1,
};

假如编译器发现有情况没被覆盖到,比如我把处理负数的分支去除:

int lightStatus = 3;

int sign = lightStatus switch
{
    > 0 => 1,
    0 => 0,
    // < 0 => -1,
};

就会抛出警告 ⚠ CS8509:switch 表达式不处理其输入类型的所有可能的值(它不是穷举)。例如,模式“-1”未包含在内。 提醒你考虑这种情况(甚至还给你举了一个“-1”的例子),或者设置一个默认分支。非常的人性化。

现在,让我们一起写一段switch表达式吧!

改变一下我们的红绿灯案例。有一辆自动驾驶汽车来到了路口,它需要根据当前是否为绿灯,以及道路上有没有行人来判断能否通过。

先定义这些变量:

int lightStatus; // 1:红灯  2:黄灯  3:绿灯
bool humanOnRoad;
bool canPassNow;

因为只有绿灯能通行嘛,所以我们设计一个对lightStatus进行判断的switch表达式,用一个“是绿灯”的常量模式分支+一个默认分支就可以了:

lightStatus switch
{
    3 => true,
    _ => false,
}

是绿灯的情况下,还得考虑有没有行人。我们用when子句补上这个附加条件:

lightStatus switch
{
    3 when !humanOnRoad => true,
    _ => false,
}

最后,把switch表达式得到的结果赋值给变量canPassNow

canPassNow = lightStatus switch
{
    3 when !humanOnRoad => true,
    _ => false,
};

大功告成!

注意分号 ②

前面说if语句和switch语句虽然是语句,但末尾不加分号。switch表达式是表达式,为什么上面的案例中却要加分号呢?

请注意了!switch表达式和其他表达式一样,末尾不用加分号。上面案例中的分号不属于switch表达式,它属于把switch表达式的结果赋值给canPassNow的赋值语句!!

三元条件运算符

在条件判断中,有一种情况特别常见。那就是:判断一个条件是否成立,如果成立,就给变量赋一个值;如果不成立,就赋另一个值。

有两个整数ab,如何找出它们之中比较大的那一个呢?用if-else语句试试:

int a = 3;
int b = 8;

int theBigger;

if (a > b)
{
    theBigger = a;
}
else
{
    theBigger = b;
}

Console.WriteLine(theBigger);

用switch表达式试试:

theBigger = a switch
{
    > b => a,
    _ => b,
};

语法糖又来了。C#为这种情况提供了一个简化语法——三元条件运算符。好家伙,我们学过一元运算符和二元运算符,这次来了个能对3个量进行操作的运算符。它的用法是这样的:

condition ? value1 : value2

condition是一个布尔表达式。首先判断condition的值是什么,如果是true,那就把value1作为运算结果返回;如果是false,则把value2作为结果返回。

这样以来,我们就能得到一个比switch表达式还要简洁的语法:

theBigger = (a > b) ? a : b;

狠狠地使用括号吧 ②

逻辑运算符的优先级高于三元条件运算符,所以上面的案例中,(a > b)的括号不是必须的。但就像之前提示过的——加上会使你的意图更清晰。

Note

末尾的分号是给theBigger赋值的语句的,不是三元条件表达式的。

Note

如果ab相等,此时a > b不成立,返回b的值。

记忆助手

是不是 ? 是的话就…… : 不是的话就……

测验时间

用三元条件表达式找到ab中比较小的那个。

查看答案

解法1:

int theSmaller = (a > b) ? b : a;

解法2:

int theSmaller = (a < b) ? a : b;

这个用法的一种应用场景是:进行一项有风险的尝试(包括加载可能出错的文件、从网络获取数据等等),如果成功了,那么皆大欢喜,就使用成果。而如果失败了,就使用备用方案。

data = isOnlineAvailable ? onlineFile : localFile;

局部变量

《论语·泰伯》&《论语·宪问》

子曰:“不在其位,不谋其政。”

在我们的一生中,总是会结识许许多多的人,同时又会有另一些人与我们渐行渐远。在程序的一生中也会结识许多实例,同时也有另一些实例被遗忘。

垃圾回收

这“遗忘”的过程还真有点类似大脑。运行时会判断哪些实例不再需要,并在合适的实际把它们销毁,释放内存。这个过程叫垃圾回收(garbage collection, GC)。

在你入职A公司之后,小a被委派为你的助手。平时,你可以让它端茶倒水,或者把麻烦的杂活都扔给它做。当你从A公司离职以后,小a也自然不再是你的下属,这时想使唤它是不可能的了。但是,无论你身处何地,父母永远是你的后盾,你可以随时和他们倾诉心声。

看看C#里是否也是这样:

int parents = 2;

Console.WriteLine(parents); // 与父母交流:🆗
Console.WriteLine(a);       // 使唤小a:❌

{ // 入职A公司
    double a = 0; // 结识小a
    a += 3.14;    // 使唤小a:🆗

    Console.WriteLine(parents); // 与父母交流:🆗
} // 从A公司离职

Console.WriteLine(parents); // 与父母交流:🆗
Console.WriteLine(a);       // 使唤小a:❌

完全符合预期。无论是入职之前还是离职以后,想使唤小a都会引发错误:当前上下文中不存在名称“a”。也就是说,a存在的“上下文”从在代码块中声明a开始(第7行),到所在代码块结束(第11行)。

如果我们在代码块里面再嵌套一个代码块:

{ // 入职A公司
    double a = 0;
    a += 3.14;

    { // 外派子公司B
        bool b = true; // 结识小b
        b = false;     // 使唤小b:🆗

        a = 0;         // 使唤小a:🆗
    } // 结束外派

    b = true; // 使唤小b:❌
    a = 10;   // 使唤小a:🆗
} // 从A公司离职

被A公司派驻到子公司B后,小b成为了临时助手,同时,a依然是你的助手。结束外派之后,b就不是你的助手了。b存在的“上下文”从在内层代码块中声明b开始(第6行),到所在代码块结束(第10行)。

看到这里,我想你应该能理解为什么叫“局部变量”了——因为它们起作用的范围是有限的。

提前了解

parents这样的变量好像没有被花括号{}括住,它们是局部变量吗?属于哪个代码块?

C#中的所有局部变量都必须在代码块里创建、储存和使用。虽然parents这样的变量看起来没有被{}包裹,但编译器在背地里还是悄悄把这个文件的所有东西包在了代码块里面。(编译器:我会稳稳地接住你😘)

是的,这也是个语法糖,我们会在第4章介绍它——顶级语句(Top-level statements)。

题外话

如果你被Rust的生命周期和作用域折磨过,这对你来说应该是小菜一碟。🦀

挑战1

声明一个表示月份的变量,分别用if-else语句、switch语句(贯穿)、switch语句(逻辑模式)和switch表达式,根据它的值判断这个月有多少天(假设是平年)。哪种写法最符合DRY原则?

参考答案

分析一下这个问题:结果可能有3种——大月(31天)、小月(30天)和2月(28天)。

(1)if-else语句

int month;
int dayNum;

if (month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12)
{
    dayNum = 31;
}
else if (month == 4 || month == 6 || month == 9 || month == 11)
{
    dayNum = 30;
}
else if (month == 2)
{
    dayNum = 28;
}
else
{
    Console.WriteLine("Invalid month!");
    dayNum = 0;
}

Console.Write("This month has ");
Console.Write(dayNum);
Console.WriteLine(" days.");

(2)switch语句(贯穿)

int month;
int dayNum;

switch (month)
{
    case 1:
    case 3:
    case 5:
    case 7:
    case 8:
    case 10:
    case 12:
        dayNum = 31;
        break;
    case 4:
    case 6:
    case 9:
    case 11:
        dayNum = 30;
        break;
    case 2:
        dayNum = 28;
        break;
    default:
        Console.WriteLine("Invalid month!");
        dayNum = 0;
        break;
}

Console.Write("This month has ");
Console.Write(dayNum);
Console.WriteLine(" days.");

(3)switch语句(逻辑模式)

int month;
int dayNum;

switch (month)
{
    case 1 or 3 or 5 or 7 or 8 or 10 or 12:
        dayNum = 31;
        break;
    case 4 or 6 or 9 or 11:
        dayNum = 30;
        break;
    case 2:
        dayNum = 28;
        break;
    default:
        Console.WriteLine("Invalid month!");
        dayNum = 0;
        break;
}

Console.Write("This month has ");
Console.Write(dayNum);
Console.WriteLine(" days.");

(4)switch表达式

int month;
int dayNum;

dayNum = switch (month)
{
    1 or 3 or 5 or 7 or 8 or 10 or 12 => 31,
    4 or 6 or 9 or 11 => 30,
    2 => 28,
    _ => 0,
};

// switch表达式不适合进行操作,因此错误提示留在这里进行
if (dayNum == 0)
{
    Console.WriteLine("Invalid month!");
}

Console.Write("This month has ");
Console.Write(dayNum);
Console.WriteLine(" days.");

显然,最后一种写法重复的部分最少,更符合DRY原则。