第 4 章:流转 — 当代码开始分岔
一个简单的任务,和它背后不简单的问题
根据订单状态决定下一步动作:
- 未支付 → 提醒用户
- 已支付 → 检查是否超过 30 天,超过则标记为"已完成",未超过则等待发货
- 已发货 → 确认收货倒计时
- 已取消 → 存档
- 未知状态 → 报警
任何程序员都能写出这段逻辑。但你有没有想过——
为什么有的语言写完就完了,有的语言编译器会追问你:"你确定你处理了所有可能的状态吗?"
这不是语法的差异。这是语言对 "逻辑完整性" 的态度。
4.1 语法基础:8 种语言如何让代码分岔
Python
# if/elif/else:Python 最日常的分支
status = order.status
if status == "unpaid":
remind_user(order)
elif status == "paid":
if order.days_since_payment() > 30:
complete_order(order)
else:
await_shipment(order)
elif status == "shipped":
start_countdown(order)
elif status == "cancelled":
archive_order(order)
else:
alert(f"unknown status: {status}")
# Python 3.10+ 的模式匹配(structural pattern matching)
match status:
case "unpaid":
remind_user(order)
case "paid" if order.days_since_payment() > 30:
complete_order(order)
case "paid":
await_shipment(order)
case "shipped":
start_countdown(order)
case "cancelled":
archive_order(order)
case _:
alert(f"unknown status: {status}")Python 的 match/case(3.10+)是相对较新的特性,比 if/elif 更擅长处理嵌套结构和守卫条件。但它不强制穷尽性——漏掉一个分支,只有运行时才会发现。
JavaScript / TypeScript
// 传统 if/else
if (status === "unpaid") {
remindUser(order);
} else if (status === "paid") {
if (order.daysSincePayment() > 30) {
completeOrder(order);
} else {
awaitShipment(order);
}
} else if (status === "shipped") {
startCountdown(order);
} else if (status === "cancelled") {
archiveOrder(order);
} else {
alert(`unknown status: ${status}`);
}
// switch(带 break,容易忘)
switch (status) {
case "unpaid":
remindUser(order);
break;
case "paid":
if (order.daysSincePayment() > 30) {
completeOrder(order);
} else {
awaitShipment(order);
}
break;
case "shipped":
startCountdown(order);
break;
case "cancelled":
archiveOrder(order);
break;
default:
alert(`unknown status: ${status}`);
}
// TypeScript 的 discriminated union + switch —— 编译器能检查穷尽性
type OrderStatus = "unpaid" | "paid" | "shipped" | "cancelled";
function handleOrder(order: { status: OrderStatus }): void {
switch (order.status) {
case "unpaid": return remindUser(order);
case "paid": return handlePaid(order);
case "shipped": return startCountdown(order);
case "cancelled": return archiveOrder(order);
// 如果漏掉一个 case,TypeScript 编译器会报错
// 因为返回类型是 void,而非 never
}
}Java
// 传统 switch(Java 17 之前)
switch (status) {
case "unpaid":
remindUser(order);
break;
case "paid":
handlePaid(order);
break;
case "shipped":
startCountdown(order);
break;
case "cancelled":
archiveOrder(order);
break;
default:
alert("unknown status: " + status);
}
// Java 17+:增强型 switch + 模式匹配
String action = switch (status) {
case "unpaid" -> {
remindUser(order);
yield "reminded";
}
case "paid" -> {
handlePaid(order);
yield "handled";
}
case "shipped" -> {
startCountdown(order);
yield "counting";
}
case "cancelled" -> {
archiveOrder(order);
yield "archived";
}
default -> {
alert("unknown status: " + status);
yield "unknown";
}
};
// Java 的 switch 表达式强制穷尽性——如果 enum switch 漏了 case,编译器报错C++
// if/else
if (status == "unpaid") {
remind_user(order);
} else if (status == "paid") {
handle_paid(order);
} else if (status == "shipped") {
start_countdown(order);
} else if (status == "cancelled") {
archive_order(order);
} else {
alert("unknown status: " + status);
}
// switch(只适用于整数和枚举类型!注意 C++ switch 不能直接用于 string)
switch (status_code) { // 需要先转成枚举或整数
case Status::Unpaid:
remind_user(order);
break; // 别忘了 break!否则 fall through
case Status::Paid:
handle_paid(order);
break;
case Status::Shipped:
start_countdown(order);
break;
case Status::Cancelled:
archive_order(order);
break;
default:
alert("unknown status");
}Rust
// match:Rust 最标志性的控制流结构
match order.status {
Status::Unpaid => remind_user(&order),
Status::Paid => {
if order.days_since_payment() > 30 {
complete_order(&order);
} else {
await_shipment(&order);
}
}
Status::Shipped => start_countdown(&order),
Status::Cancelled => archive_order(&order),
// 如果 Status 是 enum,漏写一个分支 → 编译错误
// 如果加了 #[non_exhaustive],需要通配分支
}
// if let:只匹配一个模式
if let Status::Paid = order.status {
println!("order is paid");
}
// matches! 宏:返回布尔值
let is_done = matches!(order.status, Status::Shipped | Status::Cancelled);Go
// if/else if:Go 最日常的分支
if status == "unpaid" {
remindUser(order)
} else if status == "paid" {
if order.DaysSincePayment() > 30 {
completeOrder(order)
} else {
awaitShipment(order)
}
} else if status == "shipped" {
startCountdown(order)
} else if status == "cancelled" {
archiveOrder(order)
} else {
alert("unknown status: " + status)
}
// switch:Go 的 switch 不 fall through(除非显式写 fallthrough)
switch status {
case "unpaid":
remindUser(order)
case "paid":
if order.DaysSincePayment() > 30 {
completeOrder(order)
} else {
awaitShipment(order)
}
case "shipped":
startCountdown(order)
case "cancelled":
archiveOrder(order)
default:
alert("unknown status: " + status)
}
// switch 不带表达式 = 更清晰的 if/else 替代
switch {
case amount < 0:
return fmt.Errorf("invalid amount")
case amount == 0:
return fmt.Errorf("zero amount")
default:
// proceed
}Swift
// switch:强穷尽性检查
switch order.status {
case .unpaid:
remindUser(order)
case .paid:
if order.daysSincePayment() > 30 {
completeOrder(order)
} else {
awaitShipment(order)
}
case .shipped:
startCountdown(order)
case .cancelled:
archiveOrder(order)
// 如果漏了 enum case,编译器报错
}
// guard:提前退出的 Swift 标志性语法
func process(_ order: Order) throws {
guard order.amount >= 0 else {
throw ValidationError.invalidAmount
}
// 正常流程——amount 已经确定为非负
}Kotlin
// when:Kotlin 的增强型 switch
when (status) {
"unpaid" -> remindUser(order)
"paid" -> {
if (order.daysSincePayment() > 30) completeOrder(order)
else awaitShipment(order)
}
"shipped" -> startCountdown(order)
"cancelled" -> archiveOrder(order)
else -> alert("unknown status: $status")
}
// when 作为表达式
val action = when (status) {
"unpaid" -> "remind"
"paid" -> "handle"
"shipped" -> "countdown"
"cancelled" -> "archive"
else -> "unknown"
}
// when 用于 enum 时,如果不写 else 且漏了 case,编译器报错
enum class Status { UNPAID, PAID, SHIPPED, CANCELLED }
when (status) {
Status.UNPAID -> remindUser(order)
Status.PAID -> handlePaid(order)
Status.SHIPPED -> startCountdown(order)
Status.CANCELLED -> archiveOrder(order)
// 不需要 else——编译器检查穷尽性
}语法速查:分支与循环
| 能力 | Python | TypeScript | Java | C++ | Rust | Go | Swift | Kotlin |
|---|---|---|---|---|---|---|---|---|
| if/else | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| switch/match | match/case (3.10+) | switch | switch (+ 增强版 17+) | switch (仅整数/enum) | match | switch (无 fallthrough) | switch | when |
| 穷尽性检查 | 否(match 也不强制) | TypeScript discriminated union 可 | enum switch 可 | 编译器可警告 | 强制(编译错误) | 否 | 强穷尽性 | enum when 强制 |
| 表达式 vs 语句 | 语句 + match 表达式 | 语句 + 三元 | 语句 + switch 表达式 (17+) | 语句 + 三元 | 一切皆表达式 | 语句 | 语句 + switch 表达式 | 语句 + when 表达式 |
| 守卫/条件 | if 守卫 (match) | if 在 case 中 | when 子句 | — | if 守卫 | switch 无表达式 | where | if 在 when 中 |
| 提前退出 | return | return | return | return | return / ? | return | guard | return / ?: |
4.2 深度对照一:表达式 vs 语句 — 这件事比你想象的深
在大多数语言里,if 是一个语句——它执行动作,但不返回值:
// Java(传统)
String action;
if (status.equals("paid")) {
action = "complete";
} else {
action = "pending";
}在另一些语言里,if 是一个表达式——它产生值:
// Rust
let action = if status == Status::Paid {
"complete"
} else {
"pending"
};这两种写法看起来差异不大——直到你开始嵌套。
语句世界的痛
// Java 语句风格:声明和赋值分离
String level;
if (amount > 1000) {
if (paid) {
level = "vip-premium";
} else {
level = "high-value";
}
} else {
if (paid) {
level = "standard";
} else {
level = "to-remind";
}
}你需要在脑子里同时追踪 "每个分支最后一定会给 level 赋值吗"。如果漏了一个分支,level 可能是未初始化状态——这取决于编译器的确定赋值分析(definite assignment analysis)。
表达式世界的优雅
// Rust 表达式风格:声明即赋值
let level = if amount > 1000 {
if paid { "vip-premium" } else { "high-value" }
} else {
if paid { "standard" } else { "to-remind" }
};变量声明和值绑定在一起。编译器强制每个分支都产生值——漏一个分支,编译不过。表达式风格让 "变量初始化完整" 这件事从 "靠人工检查" 变成 "靠编译器检查"。
谁选择了什么
| 语言 | if 地位 | switch/match 地位 |
|---|---|---|
| Rust | 表达式 | 表达式 |
| Kotlin | 语句 | when 可以是表达式 |
| Swift | 语句 | switch 可以是表达式 |
| Java (17+) | 语句 | switch 可以是表达式 |
| Python (3.10+) | 语句 | match 可以是表达式 |
| Go | 语句 | 语句 |
| C++ | 语句 | 语句 |
| JavaScript | 语句(三元除外) | 语句 |
为什么不是所有人都切成表达式? 因为表达式风格也有代价:它倾向于产生更多嵌套,而深层嵌套的可读性不好。你需要一个平衡——像 Rust 的 ? 操作符、Swift 的 guard、Kotlin 的 ?: 这些提前返回/提前退出的工具,让表达式风格可以不依赖深层嵌套。
4.3 深度对照二:穷尽性检查 — 编译器追问你的那些问题
把订单状态定义成枚举,然后在分支里漏掉一个 case。
Rust:不通过就别想编译
enum Status { Unpaid, Paid, Shipped, Cancelled }
fn handle(status: Status) {
match status {
Status::Unpaid => remind(),
Status::Paid => process(),
Status::Shipped => track(),
// 漏了 Cancelled → 编译错误:
// error[E0004]: non-exhaustive patterns: `Status::Cancelled` not covered
}
}Swift:同样强制
enum Status { case unpaid, paid, shipped, cancelled }
func handle(_ status: Status) {
switch status {
case .unpaid: remind()
case .paid: process()
case .shipped: track()
// 漏了 .cancelled → 编译错误:
// Switch must be exhaustive
}
}Kotlin:enum + when 强制
enum class Status { UNPAID, PAID, SHIPPED, CANCELLED }
fun handle(status: Status) {
when (status) {
Status.UNPAID -> remind()
Status.PAID -> process()
Status.SHIPPED -> track()
// 漏了 CANCELLED → 编译错误:
// 'when' expression must be exhaustive, add necessary 'CANCELLED' branch or 'else' branch instead
}
}Java:部分检查
// Java:enum switch 如果漏了 case,IDE 会警告,但不是编译器错误(除非用 switch 表达式)
String action = switch (status) { // switch 表达式强制穷尽
case UNPAID -> "remind";
case PAID -> "process";
case SHIPPED -> "track";
// 漏了 CANCELLED → 编译错误
};Python / JavaScript / Go:不检查
match status:
case "unpaid": remind()
case "paid": process()
case "shipped": track()
# 漏了 cancelled —— 完全没问题,匹配不到就什么都不做穷尽性检查的真正价值
不是你写的时候会被提醒。而是:
三个月后,有人给
Status加了一个Refunded状态。穷尽性检查会让所有需要处理新状态的地方全体报编译错误。没有被检查的语言里,这些地方会安静地带着"未知状态走 default"上线。
穷尽性匹配不是在帮你写今天的代码。它是在帮你把未来的变更风险从 "靠勇气和测试覆盖" 变成 "靠编译器强制追踪"。
4.4 深度对照三:循环的那些隐藏分岔
for 循环的基础形态(8 门语言都会)
for order in orders:
process(order)但这只是循环世界的第一层。往下看。
提前退出:break 和 continue 的普遍存在
所有 8 门语言都有 break 和 continue。但有些语言可以做得更多:
// Java:标签 break 可以跳出多层循环
outer:
for (var batch : batches) {
for (var order : batch) {
if (order.isError()) {
break outer; // 直接跳出最外层循环
}
}
}// Rust:loop 标签 + break 带值
let result = 'search: loop {
for batch in &batches {
for order in batch {
if order.is_target() {
break 'search Some(order.id.clone());
}
}
}
break None; // 没找到
};Rust 的 loop 是特殊的——它不仅支持标签 break,还能从循环中返回值。这是因为 loop 是一个表达式。
Go 的 range 是朴素的——你得不到索引以外的信息
for _, order := range orders {
// 你现在在处理第几个?如果你没在前面声明 i,你就不知道
// 后面还剩几个?不知道
// 当前是最后一个吗?不知道
}# Python 的 enumerate + 推导式可以知道更多
for i, order in enumerate(orders):
remaining = len(orders) - i - 1
is_last = (i == len(orders) - 1)// Rust 的迭代器给了你全部控制权
for (i, order) in orders.iter().enumerate() {
let remaining = orders.len() - i - 1;
let is_last = i == orders.len() - 1;
}这不是说 Python 和 Rust 的循环 "更好"。而是说 Go 刻意保持了 range 的简单性——你可以自己追踪索引,但语言不帮你做。这符合 Go 的总体哲学:给你一套最朴素的工具,你就不会写出太花哨的代码。
4.5 边界探索:跨语言控制流陷阱
陷阱一:C++ → 任何现代语言:switch 的 fall through
// C++:switch 默认 fall through
switch (status) {
case Status::Paid:
process_payment();
// 忘了 break!会继续执行下面的 Shipped 逻辑
case Status::Shipped:
start_shipping(); // 即使 status 是 Paid 也会执行这里
break;
}这可能是 C++(和 C/Java/JavaScript)最臭名昭著的设计缺陷之一。Go、Swift、Kotlin 都修复了这个问题——默认不 fall through。Rust 的 match 完全不 fall through。
陷阱二:JavaScript → Rust:没有 else if,只有 else { if }
// 从 JavaScript 来,很多人会写:
if status == Status::Paid {
handle_paid();
} else if status == Status::Shipped { // 编译错误!Rust 没有 else if 关键字
handle_shipped();
}Rust 没有 else if 关键字——但你可以组合 else 和 if:
if status == Status::Paid {
handle_paid();
} else if status == Status::Shipped { // 这其实是 else { if ... } 的缩写,恰好合法
handle_shipped();
}实际上编译器把它解成 else { if ... },效果一样但在极端缩进场景会变丑。
陷阱三:Python → Go:elif 和 else if 的肌肉记忆
# Python
if x > 0:
pass
elif x == 0: # Python 是 elif
pass// Go
if x > 0 {
// ...
} else if x == 0 { // Go 是 else if(两个词)
// ...
}陷阱四:TypeScript → Go:习惯了 switch 表达式
// TypeScript 程序员习惯的模式
const result = (() => {
switch (status) {
case "paid": return "ok";
default: return "unknown";
}
})();切到 Go 后,你会发现 Go 的 switch 不是表达式——你需要先声明变量再赋值。这是一种微妙的习惯调整:从"一切都能产生值"的世界到"有些东西只是执行"的世界。
4.6 洞察:控制流是你三个月后会不会恨自己的预告
如果你只用一门语言写分支逻辑,你不会发现:
控制流的差异不在于能不能完成逻辑——任何语言都能。差异在于三个月后,当有人(包括你自己)需要在这段代码里加一个新的分支时,语言给不给脚手架。
在没有穷尽性检查的语言里(Python、JavaScript、Go),加一个新状态意味着你需要在全代码库里人肉搜索所有 if/else 链和 switch 块,找到所有需要加新分支的地方。漏一个,就是生产 bug。
在有穷尽性检查的语言里(Rust、Swift、Kotlin、Java 17+),编译器会替你找到所有遗漏。它变成了一个变更追踪系统。
这听起来像是一个小功能。但在一个有 200 个状态判断、20 个开发者的系统里,它是维护成本的决定性差异。
编译器追问你的问题,不是不信任你。是它已经见过太多和你一样优秀、一样累、一样会忘的程序员,在上线后的凌晨被报警叫醒。
本章小结
穷尽性检查不是语法特征,是维护成本分摊制度。 今天看起来多写了一个 case,明天就少了一个凌晨报警。
表达式 vs 语句不是审美偏好。 它决定 "变量初始化是否完整" 这个问题的答案是从编译器那里来,还是从你的自律那里来。
Go 的克制和 Rust 的激进,在控制流上表现得最明显。 Go 的
switch没有穷尽性检查,for没有 enumerate。这是刻意的——Go 选择让代码一眼看得懂,代价是让静态检查帮你做的事更少。模式匹配的进化路径很清楚。 从 C 的
switch(只能匹配整数常量)到 Rust/Swift/Kotlin 的全面模式匹配(解构 + 守卫 + 穷尽性),控制流结构的表达能力提升了一个数量级。
如果你只用一门语言,你不会学到这个:
最容易写出 bug 的代码,不是复杂的代码。而是你写完觉得 "这太简单了不可能出错" 的 if/else 链——因为语言不会帮你检查它。