🎯 什麼情境該想到我
當你「有一個複雜的運算式,得盯著它拆解才知道各部分是什麼意思」的時候——把其中有意義的一段抽成有名字的區域變數,用名字說明它代表什麼,讓整個式子好讀、也好除錯。
⚙️ 怎麼用(步驟 / 公式)
意圖:把複雜運算式的一部分(或全部)抽成一個有名字的解釋性區域變數。
動作要點(小步安全進行):
- 確認要抽出的那段運算式沒有副作用。
- 宣告一個不可變的(如 const/final)新變數,把那段運算式指派給它,取一個能說明「它是什麼」的名字。
- 把原運算式中的那段替換成這個新變數。
- 編譯並測試。
- 若同一段運算式出現多次,逐一替換。
★ before / after(Go)
before:
return order.Quantity*order.ItemPrice -
math.Max(0, float64(order.Quantity-500))*order.ItemPrice*0.05 +
math.Min(order.Quantity*order.ItemPrice*0.1, 100)after——每個名字都在解釋一段運算式的意義:
basePrice := order.Quantity * order.ItemPrice
quantityDiscount := math.Max(0, float64(order.Quantity-500)) * order.ItemPrice * 0.05
shipping := math.Min(basePrice*0.1, 100)
return basePrice - quantityDiscount + shipping★ 判準:這個名字在「函式之外」也講得通嗎?
講得通(basePrice、shipping是領域詞彙)→ 其實該做 以查詢取代暫時變數 或搬到型別上變成方法。
只在這個函式裡有意義 → 停在區域變數就好。
★★ 最容易踩的失效條件:短路求值被打斷
// before:&& 的右邊只有在 u != nil 時才會求值
if u != nil && u.Profile.Active { ... }
// after:★ 壞了 —— u 是 nil 時直接 panic
active := u.Profile.Active
if u != nil && active { ... }提煉變數會把「延後求值」變成「立刻求值」。 只要被提煉的運算式
可能 panic、可能很慢、或有副作用,這個重構就不是行為保持的。
同一個陷阱的另外兩張臉:
v, ok := m[k] // map 取值本身安全
n := len(*p) // ★ 但 *p 在 p == nil 時 panic
x := arr[i] // ★ i 還沒經過範圍檢查→ 只提煉「純」的運算式。不純的,改成提煉整個 if 分支(分解條件式)。
Go 的兩個小細節
if內宣告的變數作用域只到那個if:if n := f(); n > 0 { … } else { /* n 在這裡也看得到 */ }。
提煉時把變數移出if→ 作用域變大,可能和後面的名字撞到(Go 會編譯錯誤,或更糟——靜默 shadow)。
⚠️ shadow 檢查不在go vet的預設集合裡;要靠golangci-lint把govet的shadow打開,或單獨裝golang.org/x/tools/go/analysis/passes/shadow。- 提煉變數不會讓值逃逸到 heap,除非你取了它的位址或被 closure 捕獲。用
go build -gcflags=-m看有沒有moved to heap。
→ 熱路徑上提煉變數通常零成本,不必為效能猶豫。
TypeScript
// ★ 提煉 optional chaining 時要小心:?. 也是短路的
const active = user?.profile?.active; // 安全
const n = user!.profile.active; // ★ 非空斷言把檢查關掉了,等於埋雷🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
若這個名字只在單一函式內有意義,用區域變數就好;若在整個物件範圍都值得解釋,考慮改用改變函式宣告 Change Function Declaration提出一個查詢函式。
-
若運算式本身已經夠清楚,多一個變數反而是雜訊,就別抽,甚至該反向做行內變數 Inline Variable。
-
反向手法:行內變數 Inline Variable。
-
★ 被提煉的運算式必須是「純」的(不 panic、無副作用、不昂貴),否則短路求值被打斷(見上)。
-
名字只是重述運算式就不要提煉:
isTrue := x == true、sum := a + b沒有任何資訊增益。 -
提煉出五、六個變數 → 訊號是「這個函式該拆了」,不是「命名成功了」。接著做 提煉函式。
-
迴圈內提煉變數要注意 closure 捕獲。Go 1.22 起迴圈變數改為每輪新建,行為與 1.21 以前不同——
go.mod的go版本低於 1.22 時仍是舊語義。
🔗 相關工具
- 行內變數 Inline Variable(本手法的反向操作)
- 以查詢取代暫時變數 Replace Temp with Query(把解釋性變數升級成可跨處重用的函式)
- 重構
- 以查詢取代暫時變數 —— 名字在函式外也講得通時的下一步
- 分解條件式 —— 運算式不純時的替代做法
- 行內變數 —— 反向操作
- 提煉函式 —— 提煉出太多變數時的下一步