記得很久以前,我寫註解的方式大概類似這樣:
i++; // i 加 1
現在想起來,覺得很好笑。
當時覺得「有寫註解」這件事本身就是好習慣,
至於寫了什麼⋯⋯好像不太重要?
後來進公司,第一次被 senior 在 code review 上留言:
「這個註解跟沒寫一樣。」
很直接,但他說得對。
廢話型註解,比沒有更糟
最常見的爛註解,是把程式碼翻譯成中文(或英文):
// 將 count 加 1
count++;
// 檢查 buffer 是否為 NULL
if (buffer == NULL) {
return -1;
}
// 呼叫 uart_send 發送資料
uart_send(data, len);
這些註解提供了零額外資訊。
程式碼本身就說了「count 加 1」、「檢查 NULL」、「呼叫 uart_send」。
你的註解只是把同樣的事情說了兩遍,而且還要花時間維護它。
更糟的情況是:程式碼改了,但註解忘記更新,
然後下一個人(可能是三個月後的你)看到的是互相矛盾的資訊。
好的註解在說「為什麼」
程式碼能告訴你「做了什麼」,但沒辦法告訴你「為什麼這樣做」。
這才是註解真正的價值所在。
// ❌ 說做什麼(程式碼已經說了)
// 延遲 100ms
delay_ms(100);
// ✅ 說為什麼
// 等待電源穩定,規格書 p.23 指出上電後需至少 100ms 才能操作
delay_ms(100);
// ❌
// 重試三次
for (int i = 0; i < 3; i++) {
if (send_command(cmd) == 0) break;
}
// ✅
// 硬體偶發 NAK,原因不明,實測重試三次內都能成功
// TODO: 待廠商確認根本原因 (2024-03-15)
for (int i = 0; i < 3; i++) {
if (send_command(cmd) == 0) break;
}
第二個版本,你知道這個 retry 不是隨便寫的,
是因為有個真實的硬體問題,而且還有個 TODO 提醒後續要跟進。
這才是有價值的資訊。
幾種值得寫的情況
1. 反直覺的寫法
看起來「奇怪」但有原因的程式碼,一定要解釋:
// 故意不加 break — 這裡需要 fall-through 到下一個 case
case STATE_CONNECTING:
case STATE_RECONNECTING:
start_connection_timer();
break;
// 用 volatile 讀一次來清除中斷 flag,不能優化掉這行
volatile uint32_t dummy = UART->SR;
(void)dummy;
沒有這些註解,下一個人看到會以為是 bug,然後「修掉」它。
2. 踩過的坑、已知的限制
// 注意:這個函式不是 thread-safe
// 呼叫前必須先取得 sensor_mutex
int16_t sensor_get_temperature(void) {
...
}
// buf_size 必須是 4 的倍數,否則 DMA 傳輸會對齊錯誤
// 這個限制來自硬體,不是 bug
int dma_read(uint8_t *buf, size_t buf_size) {
...
}
這種資訊藏在程式碼裡,根本看不出來。
但如果你不知道,就會踩到一樣的坑。
3. 參考來源
嵌入式開發常常要查規格書、應用筆記,
把來源記下來,之後查問題省很多時間:
// 依照 datasheet Table 5-3,ADC 轉換時間 = (SMPR + 12.5) cycles
// STM32F4 Reference Manual RM0090, p.388
uint32_t adc_sample_time = ADC_SAMPLETIME_84CYCLES;
// CRC 計算方式參考 Modbus Application Protocol V1.1b3, Section 6
uint16_t crc = modbus_crc16(frame, frame_len);
4. 暫時的 workaround
// WORKAROUND: 模組韌體 v1.2.3 有 bug,連線後需等 500ms 才能發指令
// 預計廠商 v1.3.0 修復,修復後可移除此 delay
// Ticket: #1234
delay_ms(500);
這種最重要。
沒有這個註解,六個月後沒人知道這個 delay 能不能拿掉,
結果就是永遠留在那裡,沒人敢動。
函式的 Header 註解
函式層級的註解,我習慣用簡化版的 Doxygen 格式,
不用全部都寫,但至少說清楚「這個函式在做什麼、有什麼限制」:
/**
* 從感測器讀取溫度
*
* @param timeout_ms 等待逾時時間(毫秒),0 表示不等待
* @return 溫度值(攝氏 x10,例如 235 代表 23.5°C)
* 負值代表錯誤碼
*
* @note 呼叫前需確保 sensor_init() 已完成
* @note 不是 thread-safe,多執行緒環境需自行加鎖
*/
int16_t sensor_read_temperature(uint32_t timeout_ms);
重點是 @note,把呼叫限制和注意事項寫清楚。
這些才是讓使用者不踩坑的關鍵資訊。
什麼時候不該寫註解
這個也很重要。
如果你的命名夠好,很多地方根本不需要註解:
// ❌ 命名爛,靠註解補救
int x; // 電池電壓,單位 mV
x = read_adc_channel(ADC_CH_BATTERY);
// ✅ 命名好,不需要註解
int battery_voltage_mv = read_adc_channel(ADC_CH_BATTERY);
如果你發現自己在寫「解釋這個變數是什麼」的註解,
通常代表命名可以再想一下。
後記
我現在還是偶爾會寫廢話型註解。
趕進度的時候,能動就好,哪有時間想「這個 delay 的原因是什麼」。
但我學到的是:最少也要在 workaround 和反直覺的地方寫清楚。
那些地方的一行註解,可以幫未來的人(包括自己)省掉好幾個小時的 debug 時間。
其他地方,與其寫廢話,不如.....就不寫吧。