if和switch深度解析:从执行原理到实战避坑指南
写代码这东西很多坑其实不在语法本身而在“选择结构”这种最基础的地方。if、switch几乎每个程序都在用但你随便打开一个项目的代码大概率能看到一堆可以优化的分支逻辑——要么 if 套 if 套了五六层要么 switch 里漏了 break 导致一串意外执行要么条件顺序写反结果程序跑了一整年都没人发现某个分支从来没进过。这篇内容我打算把 if 和 switch 彻底讲透。主线以 C 语言为基准因为 C 的语法最朴素、坑也最经典你把这个版本的机制弄懂了再看 Java、JavaScript、Python 这些语言里的分支结构基本就是换层皮的事。我会从它们的执行原理、适用场景、选择标准讲到实战代码和常见问题排查顺便把我在实际项目里踩过的坑、总结出来的经验一并放进来希望能帮你少走点弯路。如果你是刚学编程不久的新手这篇文章可以帮你把基础打扎实如果你已经写了几年代码也不妨扫一眼有些细节——比如 C 语言 switch 的 case 常量限制、悬空 else、短路求值带来的隐含影响——可能写代码时都未必仔细想过。1. 选择结构为什么值得重新认真学一遍1.1 从一段真实代码说起我有一次帮同事 review 代码看到一个函数作用是处理用户输入的命令大概有七八种命令每种要做的事不一样。这位同事用的思路是写一长串 if else每个分支里再嵌套好几个 if 判断参数是否合法。整个函数拉下来八十几行读起来非常吃力。我当时的评价是这代码能跑也没 bug但维护成本太高。后来我把这段逻辑整理了一下把命令分门别类做成一个 switch 结构主干清爽了很多后面再想加新命令也方便。后来我问同事为什么一开始不用 switch他说“感觉 switch 好像限制了条件不太灵活”。这就是很多人的误区。if 适合表达“条件判断”switch 适合表达“精确匹配”。两种结构没有谁更高级而是服务不同的场景。用错了代码虽然也能跑但会别扭事后维护的人会非常难受。1.2 if 和 switch 的执行机制完全不同理解一样东西先理解它的底层机制比死记语法有效得多。if 结构的核心机制是对条件表达式求值得到一个真或假的结果然后决定走哪条路。它的条件可以是任意表达式比如x 10、a b c ! 0甚至是一个函数调用只要你最终能得出布尔值就行。多个 else if 的本质是“逐个问询”第一个条件满足就不再问后面的了。switch 的核心机制是先对括号里的表达式求一次值得到一个结果然后拿这个结果和每个 case 后面的常量比较。一旦匹配上就从这个 case 开始顺序执行直到遇到 break 或者整个 switch 结束。关键在于switch 只“求一次值”然后做的是“查表匹配”所以它在应对大量离散取值时语义上更清晰性能上也可能有优势。用生活化的类比来说if 像过安检你得过好几道检查口每个口问一个问题符合就放行switch 像快递自动分拣包裹上的标签扫一下机器自动把包裹拨到对应的滑槽里。理解了这两个机制的区别后面所有细节——为什么 case 要写常量、为什么容易“穿透”、为什么 if 能做范围判断而 switch 很别扭——就都顺理成章了。2. if 语句的完整拆解与实操要点2.1 三种基础形态从最简单到最常用if 语句在 C 语言里有三种基本形态。第一种是单分支只判断一个条件为真就执行为假就跳过if (score 60) { printf(及格了\n); }第二种是双分支加上 else非此即彼if (score 60) { printf(及格了\n); } else { printf(没及格\n); }第三种是 else if 链用来处理多个互斥的条件if (score 90) { printf(优秀\n); } else if (score 80) { printf(良好\n); } else if (score 70) { printf(中等\n); } else if (score 60) { printf(及格\n); } else { printf(不及格\n); }这里提醒一点else if 不是 C 语言的关键字它本质上就是“else”后面跟着一个“if 语句”只不过写的时候省略了大括号。很多初学者会把else if当成一个整体其实拆开来看就是else { if (score 80) { ... } }理解这一点很重要因为有的新手会在else和if之间加个分号或者把else if换行写导致语法错误根本原因就是没明白它不是原子语法。2.2 悬空 else缩进骗不了编译器C 语言里有一个经典老坑叫“悬空 else”。它的核心是else 总是和离它最近的、尚未配对的 if 结合而不是和缩进对齐的那个 if 结合。if (a 0) if (b 0) printf(a 和 b 都大于0\n); else printf(这里到底是哪个 if 的 else\n);从缩进看你可能会以为这个 else 是配对外层if (a 0)的但实际编译器会把它配对内层那个if (b 0)。也就是说这段代码的逻辑其实是if (a 0) { if (b 0) { printf(a 和 b 都大于0\n); } else { printf(这里到底是哪个 if 的 else\n); } }这是个非常容易踩的坑。我见过不少初级工程师写嵌套 if 时不加大括号结果后面加需求的时候新加的 else 匹配错了层级程序行为完全变了还特别难排查。我的建议是写 if 语句时永远不要省略大括号即使分支里只有一行代码。这样既避免悬空 else 的问题也避免以后往里加代码时忘记加括号导致的逻辑错乱。2.3 短路求值既是利器也是陷阱C 语言里和||都有“短路求值”short-circuit evaluation的特性。什么意思呢A B如果 A 已经是假那么 B 根本不会被求值因为整个表达式必然是假A || B如果 A 已经是真B 也不会被求值。这个特性用好了能避免很多运行时错误用不好则是隐蔽 bug 的来源。最常见的用法是用来保护“空指针”或者“除零”比如if (p ! NULL p-value 100) { // 先判断 p 不是空再访问 p-value // 如果 p 是空第二个条件不会执行不会崩 }再比如判断一个数能否作为除数之前先判断它不为零if (b ! 0 a / b 10) { // b 为 0 时后半段不会执行不会出现除零错误 }这个特性如果被忽略就会出现问题。比如有人为了省事把条件顺序写反if (a / b 10 b ! 0) { // 先求值 a / b这里如果 b 是 0直接崩溃 }代码不会按你心里的“逻辑顺序”执行它只按表达式的书写顺序执行。所以写条件时一定要把“保护性判断”放在前面把“可能出错的访问”放在后面。这个习惯能帮你避开大量空指针崩溃。2.4 经典陷阱浮点比较、赋值号和空语句if 判断里有三个老生常谈但总是有人犯的错值得单独列出来说一下。第一个是浮点数直接比较相等。float f 0.1;然后判断if (f 0.1)很可能不成立。原因是浮点数在计算机里是二进制表示的0.1 无法被精确表示存储的是一个近似值所以直接比较相等非常不可靠。正确做法是比较它们的差是否在某个容差范围内double f 0.1 0.2; if (f - 0.3 1e-9 f - 0.3 -1e-9) { printf(f 约等于 0.3\n); }或者用fabs(f - 0.3) 1e-9更简洁。第二个是把赋值号当成等号。这个坑很多人第一天学 C 语言就踩过但踩了一次之后可能还会再踩。比如if (x 0) { // 这是把 0 赋给 x赋值表达式的值是 0即假所以这个分支永远不会进 }这个 bug 最坑的地方在于编译不报错程序也能跑但行为完全不对。不少编译器会给出警告所以开发时一定要把编译警告开起来别忽略警告信息。有个老技巧可以降低风险把常量写在左边if (0 x)如果少写一个等号变成if (0 x)编译会直接报错。这个写法对数字比较有效但不要在所有地方强行套用可读性上大家习惯不同团队内约定好即可。第三个是空语句问题。比如if (x 10); { printf(x 大于10\n); }注意 if 条件后面直接跟了一个分号分号表示 if 的语句体是一个空语句。不管条件成不成立下面那个大括号里的代码都会执行。这就是典型的“条件失效”bug。排查这种问题看到 if 行尾的分号就要警觉。这也是为什么我一直强调大括号不要省——省的时候很容易把分号看成语句的结束。2.5 条件顺序与性能的微妙关系在实际代码里else if 的分支顺序直接影响程序的性能虽然单个判断的耗时可能只是纳秒级但如果这段代码在一个大循环里执行十万次差距就会积累起来。原则是把最可能发生的情况放在最前面。比如处理 HTTP 状态码时200 是主要成功状态应该放在最前面的判断里让大部分请求只经过一两次比较就返回500 这种稀少错误放在很后面反正很少走到。另一个容易被忽略的点是条件之间有重叠时起决定性作用的判断要先写。比如根据分数分等级如果先判断score 60再判断score 90那 95 分的人早就被第一个分支拦截了根本到不了第二个分支。所以用 else if 时条件顺序要么从窄到宽要么从宽到窄必须按逻辑上有“优先级”的顺序来否则就会出现分支死代码。3. switch 语句的完整拆解与实操要点3.1 基本语法和执行流程switch 的标准写法如下switch (表达式) { case 常量1: 语句1; break; case 常量2: 语句2; break; default: 默认语句; break; }执行流程可以拆成几步先对括号里的表达式求值得到一个结果。然后从上到下把结果和每个 case 后面的常量进行比较。如果匹配到某个 case就从这个 case 后的第一条语句开始执行一直执行到 break 跳出整个 switch。如果没有匹配到任何 case就执行 default 后面的语句。这段流程里最关键的一点是匹配成功后是从当前 case 开始“往下顺序执行”而不是只执行这一条。这就是为什么会发生“case 穿透”如果你在某个 case 后面忘了写 break程序不会自动停下而是会继续执行下一个 case 的语句直到遇到 break 或 switch 结束。3.2 case 穿透最经典的 switch 坑很多人第一次被 switch 坑就是漏了 break。看这个例子int n 2; switch (n) { case 1: printf(one\n); case 2: printf(two\n); case 3: printf(three\n); default: printf(other\n); }猜猜输出是什么不是简单的 “two”而是two three other因为 n 匹配 case 2 之后后面的每个 case 也跟着执行了。这就是穿透。但穿透并不总是坏事。“故意穿透”是一个常见的合法用法用来把多个值合并到同一段处理逻辑。比如switch (month) { case 1: case 3: case 5: case 7: case 8: case 10: case 12: printf(这个月有31天\n); break; case 4: case 6: case 9: case 11: printf(这个月有30天\n); break; case 2: printf(2月比较特殊\n); break; default: printf(无效月份\n); break; }这种写法把 case 1、3、5 等合并到同一个处理分支非常自然读代码的人也知道你是故意这么写的。所以关键不在于“能不能穿透”而在于要么每个 case 都以 break 结尾要么用注释明确标注“故意不写 break让多个 case 合并”。不加注释也不写 break多半是 bug。3.3 C 语言 switch 的两个硬性限制C 语言的 switch 有两个限制跟 if 相比是明显的短板。第一个限制case 后面的值必须是整型常量表达式。整型包括 int、char、枚举等但不包括浮点数也不能是变量。下面的写法是错误的double d 0.5; switch (d) { // 错误switch 表达式不能是 double case 0.5: // 错误case 不能是浮点数 ... }char 能用于 switch 是因为 char 本质上也是整型。比如判断用户输入的字符char c getchar(); switch (c) { case y: case Y: printf(确认\n); break; case n: case N: printf(取消\n); break; default: printf(无效输入\n); break; }第二个限制容易踩坑const变量不是“常量表达式”。在 C 语言中const int a 3;只是说 a 的值不能改但编译器不认为它可以作为 case 的标签值。要定义真正可用于 case 的常量得用宏定义或者枚举#define CMD_OPEN 1 #define CMD_CLOSE 2 enum { CMD_START 1, CMD_STOP 2, CMD_RESET 3 };很多初学者在这个地方卡过明明const int x 1;写得好好的放到 case 后面编译直接报错。理解“常量表达式”这个概念以后就不会再被这个问题困扰了。3.4 实战用 switch 写一个迷你状态机switch 特别适合实现状态机因为状态机的核心就是“当前状态 事件 → 下一个状态”。这种场景天然是离散匹配。假设我们要写一个简单的开关控制流程设备有四个状态待机IDLE、运行RUNNING、暂停PAUSED、停止STOPPED事件有启动START、暂停PAUSE、恢复RESUME、停止STOP。用枚举定义状态和事件enum State { IDLE, RUNNING, PAUSED, STOPPED }; enum Event { START, PAUSE, RESUME, STOP }; enum State next_state(enum State current, enum Event event) { switch (current) { case IDLE: if (event START) return RUNNING; break; case RUNNING: if (event PAUSE) return PAUSED; if (event STOP) return STOPPED; break; case PAUSED: if (event RESUME) return RUNNING; if (event STOP) return STOPPED; break; case STOPPED: if (event START) return RUNNING; break; default: break; } return current; // 没有合法转移保持原状态 }外层用 switch 处理不同状态内层用 if 判断在当前状态下哪个事件有效。这种“switch 分状态 if 判事件”的组合比一长串 if else if 容易读得多加了新状态、新事件也容易定位到对应位置修改。如果事件的处理逻辑比较复杂还可以再用一层 switch 对事件做分发但两层 switch 嵌套可读性会降低。更大的项目里更推荐用函数指针表或者表驱动的方式这个我在后面第 4 节会专门讲。4. if 和 switch 如何选型场景对照与组合用法4.1 选择标准不是凭感觉而是看数据结构我见过不少人写分支的时候下意识用 if不管什么场景。其实选型标准很简单核心就一条if 适合范围与复合条件判断switch 适合离散值精确匹配。具体来说满足这些条件时优先考虑 switch判断的依据是同一个变量或表达式的取值。取值是离散的、有限的。取值的数量大于两三个。每个取值对应的处理逻辑相对独立。满足这些条件时用 if 更合适判断的依据涉及多个变量。条件是范围比较比如x 10 x 50。条件本身是复合逻辑比如(a 0 b ! 0) || c。比较的值是浮点数或者无法枚举的字符串C 语言中Java 和 JS 的 switch 支持字符串这是语言差异。比如根据分数判断等级这个用 if 就很自然因为它是一个连续范围如果你非要用 switch就得列一堆 case 把每个分数枚举出来从 0 到 100既笨拙又容易漏。反过来处理菜单命令、协议类型、操作符优先级这类离散值switch 则更清晰、修改也更集中。4.2 什么时候绝对不要用 switchC 语言里有一个场景绝对不要用 switch你要比较的是字符串。C 语言的字符串是 char 数组不是基本类型不能作为 switch 表达式也没法直接用比较。很多 C 项目里对字符串命令的处理是这么写的if (strcmp(cmd, open) 0) { do_open(); } else if (strcmp(cmd, close) 0) { do_close(); } else if (strcmp(cmd, reset) 0) { do_reset(); } else { error(); }这种写法本身没什么问题但如果命令数量很大一长串 strcmp 会显得重复。更优雅的方案是查表把命令字符串和对应的处理函数放在一张表里用循环匹配后面加命令只需要往表里加一行。另一个不要用 switch 的场景是判断边界值容易出错的业务规则。比如判断年份是否是闰年条件里有“能被 4 整除但不能被 100 整除或者能被 400 整除”这种公式用 if 表达直接清晰硬套 switch 只会把代码搞得没人能看懂。4.3 一种被低估的替代方案查表法当 case 数量非常多或者 case 的处理逻辑只是“把值映射到另一个值”的时候其实可以整段 switch 都不写直接用查表法。最简单的查表是把映射关系放在数组里。比如把星期数字1~7映射成中文名字const char *names[] {, 周一, 周二, 周三, 周四, 周五, 周六, 周日}; int day 3; if (day 1 day 7) { printf(%s\n, names[day]); } else { printf(无效星期\n); }比写七个 case 的 switch 简洁得多而且当天数增加到几十个数组长度跟着加就行代码结构不变。前提是映射关系的下标必须是连续的整数这样才能直接用数组下标。对于更复杂的命令处理可以用“函数指针数组 结构体表”的方式。比如一个简易的命令处理器struct Command { int id; void (*handler)(void); }; void cmd_open(void) { printf(执行打开\n); } void cmd_close(void) { printf(执行关闭\n); } void cmd_reset(void) { printf(执行重置\n); } struct Command cmd_table[] { {1, cmd_open}, {2, cmd_close}, {3, cmd_reset}, }; void process_command(int id) { size_t i; for (i 0; i sizeof(cmd_table) / sizeof(cmd_table[0]); i) { if (cmd_table[i].id id) { cmd_table[i].handler(); return; } } printf(未知命令 %d\n, id); }这样做的好处是新增命令时不需要改动 process_command 主逻辑只需要往表里加一行。这种“对修改关闭、对扩展开放”的效果在代码维护上价值很高。反过来如果使用 switch每加一个命令就要在 switch 里加一个 case主函数会越变越长最后成为一个几百行的“神仙函数”。5. 多语言横评Java、JavaScript、Python 是怎么玩分支的5.1 Java 里的增强版 switchJava 的 switch 在传统语法上和 C 语言非常像同样有 case 穿透需要写 breakcase 值最早只能是常量。但 Java 14 之后推出了增强版 switch语法和语义都变化很大String type switch (day) { case 1, 2, 3, 4, 5 - 工作日; case 6, 7 - 周末; default - 无效日期; };这里有几个重要变化一是箭头-后面直接是返回值或表达式不再需要 break也不会有穿透多个值可以用逗号合并到同一个分支整个 switch 可以作为一个表达式“产生一个值”赋给变量。这个语法比 C 语言老版 switch 安全很多可读性也更好。Java 里还有一个细节和 C 语言不同Java 的 switch 支持字符串。你可以写switch (cmd) { case open: ... }这在 C 语言里做不到。字符串匹配底层是用 hash 实现的挺高效。5.2 JavaScript 的 switch穿透还在比较是全等JavaScript 的 switch 语法跟 C 语言一脉相承同样有 case 穿透问题需要写 break。但它有两个细节值得注意第一个细节是 switch 的比较是全等比较不是宽松相等。这意味着 case 里写数字 1和字符串 1 不会被匹配。比如let n 1; switch (n) { case 1: console.log(数字1); break; case 1: console.log(字符串1); break; }这里匹配的是第二个 case。如果你以为它匹配了第一个逻辑就错了。第二个细节和 C 语言一样会坑人穿仍然存在。很多 JS 开发者写 switch 时漏了 break程序行为随之失控。我个人在 JavaScript 里写 switch 的建议是确认每个 case 都以 break、return 或 throw 结尾想合并多个 case 时显式地把 case 堆在一起让人一眼看出这是故意的。5.3 Python 没有 switch但它有 match-case很多从 C 语言转 Python 的人都会问Python 为什么没有 switch答案很长但简单说就是 Python 的设计哲学是“尽量只有一种方式来做一件事”而传统 switch 能用 if/elif 替代就没有引入。不过 Python 3.10 引入了 match-case 语句虽然不是传统意义上的 switch但功能上能覆盖大量类似场景command open match command: case open: print(执行打开) case close: print(执行关闭) case _: print(未知命令)注意 match-case 的每个分支不需要 break匹配成功后不会穿透。case _相当于 default。更强大的是它支持“结构化模式匹配”可以直接解包元组、匹配类型比如def process(point): match point: case (0, 0): print(原点) case (0, y): print(f在 y 轴上y{y}) case (x, y) if x y: print(x 等于 y) case (x, y): print(f普通点 {x}, {y})这种模式匹配能力比传统 switch 强太多了。但在 Python 里面如果你的场景只是简单离散值匹配很多人依然习惯用字典映射handlers { open: do_open, close: do_close, reset: do_reset, } handler handlers.get(command, unknown) handler()这种写法贴近前面说的查表法在很多场景下比 match-case 更简洁。5.4 Go 和 Rust 的另类思路Go 语言里没有三元运算符和 if 相关的坑少了一个但它的 switch 很有意思每个 case 默认自动 break不需要你写还允许 case 后面跟多个条件用逗号分隔。最特别的是Go 的 switch 允许不带表达式直接当 if-else-if 链用switch { case score 90: fmt.Println(优秀) case score 80: fmt.Println(良好) default: fmt.Println(加油) }这种写法等于给 if-else-if 链换了个更清晰的壳条件仍然是范围判断。Rust 更进一步用的是 match 表达式强制要求穷尽所有可能否则编译不通过。这种设计从编译器层面杜绝了“漏掉某种情况”的 bug对代码完整性要求极高。这两种语言的思想说明了现代语言在分支结构上考虑的问题更多是“可读性”和“安全性”而不只是语法便利。下面这个表可以一目了然语言是否支持传统 switch是否容易穿透是否支持字符串 case增强特性C是是漏 break 即穿透否无case 必须整型常量Java是旧版会新版箭头语法不会是switch 作为表达式返回值JavaScript是是是全等比较Python无传统形式不减match-case 不穿透match 支持结构化模式匹配Go是默认不穿透是无表达式 switch当 if 链用Rust无传统形式不适用match 支持模式匹配必须穷尽6. 常见问题与排查技巧实录6.1 分支行为异常排查思路写选择结构代码遇到“结果不对”的时候先别急按下面的思路排查效率最高问题是“某些代码似乎没执行”或者“多执行了一段”优先检查 if 条件、大括号匹配和 switch 的 break。如果是 else if 链把所有可能的分支条件都列出来对比一下有没有重叠、有没有没覆盖到的区间。特别是用、这些边界条件时把边界值代入算一算。我在一个项目里碰到过有趣的 bug判断月份天数时条件写成了if (month 7 month % 2 1)结果把 8 月以后的处理搞混了原因就是没有想清楚“1-7 月奇偶规律”和“8-12 月反过来”这个分界。用表格列出每个月的实际天数再对照代码条件问题立刻就能看出来。排查时有一个非常有用的技巧在 key 分支里插入临时打印或日志。不要觉得这一步啰嗦。打印每个分支的命中情况和关键变量值很快就能定位是哪个条件判断出了问题比盯着代码干想快得多。6.2 常见问题速查表常见问题出现原因解决思路条件为真但分支没执行表达式逻辑错误或变量值和你以为的不一致打印条件表达式中每个变量的值逐项核对else 匹配错分支悬空 else所有 if 都加大括号嵌套时显式用花括号界定范围switch 多执行了后面的 case漏写 break检查每个 case 尾部确认 break 或 returnswitch 匹配不上任何 casecase 值和表达式类型不匹配确认类型C 语言中确认 case 是整型常量JS 中注意是全等比较编译报“case label does not reduce to an integer constant”C 里 case 写了变量或 const 变量用宏定义或枚举代替浮点判断相等不成立浮点近似存储用fabs(a - b) epsilon比较条件顺序错了导致永远走到同一分支else if 顺序与优先级不符重新梳理条件优先级把决定性判断放前面在一段代码里加条件后新逻辑没生效编译器优化或缓存了旧二进制先确认重新编译再用 print 验证是否进入新分支6.3 可读性细节default 位置、break 约定关于 default 的位置老派写法喜欢放最后但现在很多人建议把 default 放在第一个尤其是在处理命令分发、错误优先的场景。理由是你希望程序遇到“未知值”时立刻发现并处理而不是一路匹配到最后才发现不匹配。这种写法读代码的人一进 switch 就知道未知情况会走到哪安全感强不少。当然default 放最后也有它的道理——顺着 case 列表读下来最后的兜底逻辑很自然。我的建议是团队约定统一即可关键是不要混乱。如果项目已经统一了某个风格就跟着走不要一个人特立独行。break 的约定就更重要了。我在代码评审时发现过太多穿透 bug后来我们团队统一约定每个 case 的代码结束必须显式写 break 或 return不允许依赖“最后一个 case 不写 break 也不会出问题”这种写法。虽然最后一个 case 在 break 上确实可以不写但一旦以后有人在这个 case 后面新增一个 case原来的代码就在无意识中产生了穿透 bug。6.4 条件别写复杂了早返回是最大功臣最后分享一个我自己的习惯写 if 嵌套时尽量用“早返回”策略把异常和边界情况提前处理掉减少嵌套层级。看一个典型例子。原始代码长这样if (p ! NULL) { if (p-type TYPE_A) { if (p-size 0) { do_process_a(p); } } }嵌套了三层读起来很费劲。用早返回改一下if (p NULL) { return; } if (p-type ! TYPE_A) { return; } if (p-size 0) { return; } do_process_a(p);逻辑等价但每一层都是平铺的读代码时不需要维护“我现在在第几层括号里”的心智状态。尤其在函数前面有一堆前置条件校验时这种写法非常舒服新来的同事看得也明白。这其实不是 if 本身的问题而是代码风格问题但它能让 if 用起来舒服得多。我在实际项目里还有一个习惯如果一个函数里分支判断超过三四个而且每个分支逻辑都比较长就把分支里的逻辑提取成单独的函数。这样 main 函数只保留“决策”的部分每个分支做什么一目了然也不用跳到一半发现这个函数已经读不完。选择结构用得好不好决定了一个人写复杂逻辑时会不会把自己绕晕也决定了别人看你代码时是觉得“这个入口很清楚”还是“这地方没人敢动”。这些经验都不是什么高深理论多数是在一次次排查 bug 和代码评审里磨出来的。下次写 if 和 switch 之前先停顿两秒想清楚这里是“范围判断”还是“离散匹配”再动手代码质量会明显不一样。