🎯 什麼情境該想到我
當你「一個區域變數的名字並沒有比它所指的運算式帶來更多意義」的時候——直接用運算式取代這個變數,去掉多餘的一層,讓程式更精簡,也常常是為了讓後續重構更好做。
⚙️ 怎麼用(步驟 / 公式)
意圖:當變數名沒比運算式本身帶來更多意義時,用運算式取代它。
動作要點(小步安全進行):
- 確認指派給該變數的運算式沒有副作用。
- 若該變數尚未宣告為不可變,先設為不可變並測試,確認它只被賦值一次。
- 找出第一處使用該變數的地方,換成它的運算式。
- 編譯、測試。
- 逐一替換其餘使用處,每次都測試。
- 全部替換後刪除該變數的宣告與賦值。
★ before / after(Go)
// before:名字沒比運算式本身清楚
basePrice := anOrder.BasePrice()
return basePrice > 1000
// after
return anOrder.BasePrice() > 1000什麼時候該 inline 掉一個變數
| 訊號 | 例子 |
|---|---|
| 名字只是重述運算式 | basePrice := o.BasePrice() |
| 只用一次,且緊接在後 | err := f(); if err != nil { → if err := f(); err != nil { |
| 妨礙下一步重構 | 要 搬移函式 前,先把只為了它存在的暫時變數消掉 |
★ Go 的慣用寫法本身就是這個手法:
// ✅ Go 慣例:把 err 收進 if 的作用域,離它被用到的地方最近
if err := db.Ping(ctx); err != nil {
return fmt.Errorf("ping: %w", err)
}★★ 失效條件:求值次數與時機都變了
這是 提煉變數 陷阱的鏡像。變數求值一次;inline 之後,每個出現的地方都求值一次。
// before:now 整段流程都是同一個時間點
now := time.Now()
rec.CreatedAt, rec.UpdatedAt = now, now
// after:★ 壞了 —— 兩個欄位會差幾奈秒,測試裡的 == 比較開始隨機失敗
rec.CreatedAt, rec.UpdatedAt = time.Now(), time.Now()只要右側是這其中之一,就不要 inline:
- 不確定的來源:
time.Now()、rand.Int()、uuid.New() - 有副作用:
ch <- v、counter.Add(1)、讀取io.Reader(★ 讀兩次 = 拿到不同資料) - 昂貴:DB 查詢、HTTP 呼叫、正則編譯
Go 的具名回傳值
func f() (result int) {
result = compute()
return result // ★ 這個 return result 不能 inline 成 return compute()
} // 如果有 defer 在改 result 的話有 defer 在動具名回傳值時,return x 和 return f() 的語義不同——先確認沒有 defer 碰它。
工具支援
gopls 有 refactor.inline.variable,且被官方列為較嚴謹的操作。仍會掉註解。
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
若變數名有解釋意圖的作用、或運算式重複計算成本高、或它讓除錯更容易,就保留變數,甚至反向做提煉變數 Extract Variable。
-
反向手法:提煉變數 Extract Variable。
-
★ 右側不是純運算式就不要 inline:
time.Now()、rand、讀io.Reader、DB/HTTP 呼叫——求值次數變多就壞了(見上)。 -
inline 後那行超過 100 字元就別做。目的是更好讀,不是更少行。
-
有
defer在修改具名回傳值時,return x不等於return f()。 -
不要順手做。行內變數常是為了「解開下一步重構的路」;如果沒有下一步,多半是在製造 diff 噪音。
🔗 相關工具
- 提煉變數 Extract Variable(本手法的反向操作)
- 行內函式 Inline Function(同樣是去掉不必要的間接,只是對象是函式)
- 重構
- 提煉變數 —— 反向操作,失效條件正好互為鏡像
- 行內函式 —— 函式層級的同一件事
- 以查詢取代暫時變數 —— 另一條路:不是刪掉變數,而是換成方法