有一種 bug,不是今天的你造成的,
是三個月後的你造成的。
你現在寫了一個 switch,
處理三種狀態,邏輯完全正確,
測試也都過了。
三個月後,需求改了,
新增了第四種狀態。
你在 enum 裡加了一個值,
但忘記去更新那個 switch。
編譯過了,沒有警告,
程式跑起來,遇到第四種狀態的時候,
switch 什麼都不做,
靜靜地繼續往下執行。
症狀可能是某個功能沒有反應,
可能是某個變數沒有被更新,
可能要跑很久才會觸發那個狀態,
然後你花了半天才找到原因。
default 加一行,可以讓這種問題立刻現形。
最簡單的例子
typedef enum {
STATE_IDLE = 0,
STATE_RUNNING = 1,
STATE_ERROR = 2,
} SystemState;
void handle_state(SystemState state) {
switch (state) {
case STATE_IDLE:
do_idle();
break;
case STATE_RUNNING:
do_running();
break;
case STATE_ERROR:
do_error();
break;
// 沒有 default
}
}
現在加了一個新狀態:
typedef enum {
STATE_IDLE = 0,
STATE_RUNNING = 1,
STATE_ERROR = 2,
STATE_SHUTDOWN = 3, // 新增
} SystemState;
handle_state 沒有更新,
STATE_SHUTDOWN 進來之後,
switch 靜靜地什麼都不做。
default 的兩種用途
用途一:抓到不應該出現的值
如果 switch 應該處理所有合法的情況,
default 就是用來抓「不合法」或「沒預期到」的值:
void handle_state(SystemState state) {
switch (state) {
case STATE_IDLE:
do_idle();
break;
case STATE_RUNNING:
do_running();
break;
case STATE_ERROR:
do_error();
break;
default:
// 走到這裡代表有 bug:
// 要嘛 state 的值不合法,
// 要嘛 enum 新增了值但這裡沒有更新
assert(0); // 讓問題立刻現形
LOG_ERROR("Unhandled state: %d", state);
break;
}
}
這樣,STATE_SHUTDOWN 進來之後,
assert(0) 立刻觸發,
你馬上知道哪裡沒有更新。
用途二:處理「其他所有情況」
有時候 switch 本來就不需要處理所有值,
default 是正常的 fallback:
void process_command(uint8_t cmd) {
switch (cmd) {
case CMD_START:
do_start();
break;
case CMD_STOP:
do_stop();
break;
case CMD_RESET:
do_reset();
break;
default:
// 收到不認識的指令,正常情況,忽略或回錯誤
LOG_WARN("Unknown command: 0x%02X", cmd);
send_error_response(ERR_UNKNOWN_CMD);
break;
}
}
這裡 cmd 來自外部(網路封包、UART),
收到不認識的指令是正常情況,
default 是合理的錯誤處理,不是 assert。
兩種情況怎麼判斷用哪個
判斷標準很簡單:
switch 的值來自哪裡?
| 來源 | 適合的 default |
|---|---|
內部的 enum,理論上只有合法值 |
assert(0) + log |
| 外部輸入(封包、使用者、感測器) | 正常的錯誤處理 |
| 混合(內部 enum 但可能有非法值) | 範圍檢查 + assert |
讓編譯器幫你抓
GCC 和 Clang 有一個很有用的警告:
gcc -Wswitch your_code.c
# 或是
gcc -Wall your_code.c # -Wall 包含 -Wswitch
-Wswitch 的效果:
當 switch 的對象是 enum,
而且有 enum 的值沒有對應的 case,
編譯器會警告。
warning: enumeration value 'STATE_SHUTDOWN' not handled in switch
這個警告非常有用,
但有一個前提:switch 不能有 default。
如果有 default,編譯器會認為你已經處理了所有情況,
就不會警告了。
所以要怎麼兩全其美?
這裡有一個矛盾:
- 有
default→ 編譯器不警告漏掉的enum值 - 沒有
default→ 漏掉的值靜靜地什麼都不做
解法是把兩件事分開:
void handle_state(SystemState state) {
switch (state) {
case STATE_IDLE:
do_idle();
break;
case STATE_RUNNING:
do_running();
break;
case STATE_ERROR:
do_error();
break;
case STATE_SHUTDOWN:
do_shutdown();
break;
// 不加 default,讓編譯器幫你警告漏掉的 enum 值
}
// 在 switch 之外加一個防禦
// 如果有不合法的值(不是 enum 裡的任何一個),
// 上面的 switch 什麼都不做,這裡可以加額外的檢查
}
或是用 GCC 的 __attribute__:
void handle_state(SystemState state) {
switch (state) {
case STATE_IDLE: do_idle(); break;
case STATE_RUNNING: do_running(); break;
case STATE_ERROR: do_error(); break;
case STATE_SHUTDOWN:do_shutdown(); break;
default:
// 這裡只會被不合法的值觸發
// (不是 enum 裡的任何一個,例如被強制轉型塞進來的值)
assert(0);
break;
}
}
然後開啟 -Wswitch-enum(比 -Wswitch 更嚴格):
gcc -Wswitch-enum your_code.c
-Wswitch-enum 即使有 default,
也會警告漏掉的 enum 值,
兩全其美。
幾個常見的壞習慣
壞習慣一:default 裡什麼都不做
switch (state) {
case STATE_IDLE: do_idle(); break;
case STATE_RUNNING: do_running(); break;
default:
break; // ❌ 什麼都不做,等於沒有 default
}
這樣的 default 沒有意義,
漏掉的狀態還是靜靜地被忽略。
壞習慣二:忘記 break 造成 fall-through
switch (state) {
case STATE_IDLE:
do_idle();
// ❌ 忘記 break,會繼續執行 STATE_RUNNING 的程式碼
case STATE_RUNNING:
do_running();
break;
case STATE_ERROR:
do_error();
break;
}
C 的 switch 不加 break 會 fall-through,
有時候是刻意的,但更多時候是忘記加。
如果是刻意的 fall-through,加個註解說明:
switch (state) {
case STATE_IDLE:
do_idle();
/* fall through */ // 明確說明這是刻意的
case STATE_RUNNING:
do_running();
break;
}
GCC 支援 __attribute__((fallthrough)) 或 [[fallthrough]](C23),
可以讓編譯器知道這是刻意的,不要警告:
case STATE_IDLE:
do_idle();
__attribute__((fallthrough));
case STATE_RUNNING:
do_running();
break;
壞習慣三:switch 裡宣告變數沒有加大括號
switch (state) {
case STATE_IDLE:
int x = 10; // ❌ 某些編譯器會報錯或行為奇怪
do_something(x);
break;
case STATE_RUNNING:
// x 在這裡的作用域是什麼?
break;
}
在 case 裡宣告變數,要用大括號包起來:
switch (state) {
case STATE_IDLE: {
int x = 10; // ✅ 作用域明確
do_something(x);
break;
}
case STATE_RUNNING:
break;
}
狀態機的 switch 特別重要
嵌入式開發裡,狀態機幾乎無所不在,
switch 是實作狀態機最常見的方式。
狀態機的 switch 如果沒有處理好,
系統可能進入一個「沒有任何 case 處理」的狀態,
然後靜靜地卡在那裡,什麼都不做,
或是繼續往下執行,做出奇怪的行為。
typedef enum {
CONN_STATE_DISCONNECTED = 0,
CONN_STATE_CONNECTING = 1,
CONN_STATE_CONNECTED = 2,
CONN_STATE_RECONNECTING = 3,
} ConnState;
void connection_fsm(ConnState state, Event event) {
switch (state) {
case CONN_STATE_DISCONNECTED:
handle_disconnected(event);
break;
case CONN_STATE_CONNECTING:
handle_connecting(event);
break;
case CONN_STATE_CONNECTED:
handle_connected(event);
break;
case CONN_STATE_RECONNECTING:
handle_reconnecting(event);
break;
default:
// 不合法的狀態,這不應該發生
LOG_ERROR("Invalid connection state: %d", state);
assert(0);
break;
}
}
每次新增狀態,
-Wswitch-enum 會提醒你更新這個 switch,
default 的 assert 會在漏掉的時候立刻告訴你。
雙重保護。
說實話
default 這件事,
說起來是一個很小的習慣,
但我見過不少因為沒有 default 而產生的 bug,
都是「新增了 enum 值,但某個 switch 沒有更新」。
這種 bug 特別難找,
因為程式不會 crash,
只是某個功能在特定情況下沒有反應,
或是行為不符合預期。
我現在的習慣是:
- 對內部 enum 的
switch,一定加default: assert(0) - 同時開
-Wswitch-enum,讓編譯器也幫我盯著 - 對外部輸入的
switch,default是正常的錯誤處理
兩種情況分開對待,
不會因為「這個 switch 有 default 了」就放鬆警惕。
實戰 Checklist
- [ ] 所有
switch都有default - [ ] 對內部 enum 的
switch,default有加assert(0)或 log - [ ] 對外部輸入的
switch,default有正常的錯誤處理 - [ ] 開啟
-Wswitch或-Wswitch-enum警告 - [ ] 刻意的 fall-through 有加
/* fall through */或[[fallthrough]]註解 - [ ]
case裡宣告變數有用大括號包起來 - [ ] 狀態機的
switch有涵蓋所有合法狀態