Switch 语句的语义

switch 语句一般用于多重分支选择. 不过 switch 的语义与 if ... else if 完全不同, 它更像是 goto 语句. switch 只接一个语句块, 语句块中可以包含一些 case 跳转标签, switch 先对表达式求值, 然后跳转至对应的标签. break 可以跳出 switch 语句.

1
2
3
4
5
6
7
8
switch (state) {
case 1:
foo();
break;
case 2:
bar();
break;
}

因为 switch 语句是简单粗暴的跳转, 所以它并没有什么结构性可言. 所以我们可以写出一些奇奇怪怪的 switch 语句:

1
2
3
4
5
6
7
8
int n, i = 0;
switch (std::cin >> n, n) {
default:
for (i = n; i < 10; ++i) {
case 0:
std::cout << i << std::endl;
}
}

上面的代码完全合法, 类似 goto, 跳转是无所谓层级结构的, 因此当 n 等于 0 时, 跳转至循环体内部也是完全合法的. 也许是因为这种用法比较 hack, 很少有教材和教程会告诉你 switch 语句还能这么用. 这一特性的一个比较著名的应用就是 Duff’s device:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
send(to, from, count)
register short *to, *from;
register count;
{
register n = (count + 7) / 8;
switch (count % 8) {
case 0: do { *to = *from++;
case 7: *to = *from++;
case 6: *to = *from++;
case 5: *to = *from++;
case 4: *to = *from++;
case 3: *to = *from++;
case 2: *to = *from++;
case 1: *to = *from++;
} while (--n > 0);
}
}

这是风格非常古老的 C 代码. 它所做的是将 from 指向的数据复制到 to, 中, 一共有 count 个. 如果使用常规的写法, while 循环要执行 count 次, 表达式 --n > 0 也需求值 count 次. Duff’s device 的做法是让循环执行 \(\lceil \frac{\mathrm{count}}{8} \rceil\) 次, 如果 count 不能被 8 整除, 则会在第一次循环时用 switch 语句跳转至循环体的相应位置, 让第一次循环时 *to = *from++ 执行 count % 8 次; 再加上接下来的 \(\lfloor \frac{\mathrm{count}}{8} \rfloor\) 次循环, 保证赋值语句始终执行 count 次. 这样 while 的条件判断次数减少为常规做法的 1/8.

正是因为这种可随意跳转的特性, C/C++ 就必须对其做一些限制. 我们不能在 switch 语句块中随意初始化变量:

1
2
3
4
5
6
7
8
switch (state) {
case 1:
int a = foo(); // error: jump bypasses variable initialization
break;
case 2:
bar();
break;
}

这是因为整个 switch 的语句块是同一个作用域, 因此变量 a 的作用域包括 case 2:. 如果 state 为 2, case 2: 中就能访问到一个未初始化都变量 (虽然它没这么做). 这个规则与 goto 语句一致:

1
2
3
4
goto next
int a = foo(); // error: jump bypasses variable initialization
next:
bar();

解决办法是手动限制变量的作用域, 让变量只存在于特定 case 中:

1
2
3
4
5
6
7
8
9
10
switch (state) {
case 1: {
int a = foo();
break;
}
case 2: {
bar();
break;
}
}

如果只是声明变量而没有初始化它, 则也是合法的. 这与 goto 语句一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
switch (state) {
int a, b;
case 1:
a = 1;
break;
case 2:
b = 2;
break;
}

goto next
int c;
next:
foo(c = 1);

因此与其称 switch 为 “多分支选择语句”, 不如称其为 “条件跳转语句” – 与之对应的 goto 是无条件跳转语句. 正是因为这种语义比较 hack, 所以后来新出的一些语言虽然有 switch 语句, 但是语义完全不同. 例如 C# 的 switch 语句要求 case 必须对应一个 break, 除非这个 case 为空; Go 的 switch 语句的每个 case 都是一个明确的分支. 这样的 switch 语句才能光明正大地称为 “多分支选择语句”.

Python 中的 if ... elif 也可以认为是多分支选择语句:

1
2
3
4
5
6
if a == 1:
foo()
elif a == 2:
bar()
elif a == 3:
baz()

其中各个 ifelif 的地位是等同的, 它的语法树是这样的:

1
2
3
4
5
6
7
8
9
10
if
├── case 1
│ ├── condition: a == 1
│ └── statements...
├── case 2
│ ├── condition: a == 2
│ └── statements...
└── case 3
├── condition: a == 3
└── statements...

而 C/C++ 中 if ... else if 这样的用法则不能认为是多分支选择语句, 因为它的语义实际上是嵌套 if-else. 它的语法树是这样的:

1
2
3
4
5
6
if: a == 1
├── statements...
└── if: a == 2
├── statements...
└── if: a == 3
└── statements...

Switch 语句的语义
https://luyuhuang.tech/2021/11/14/switch-statement.html
Author
Luyu Huang
Posted on
November 14, 2021
Licensed under