🎯 什麼情境該想到我

當你看到「一個迴圈同時做了好幾件不相干的事」(一邊算總和、一邊找最小值、一邊組字串)的時候,改其中一件事總要小心翼翼繞過另一件。

⚙️ 怎麼用(步驟 / 公式)

意圖:讓每個迴圈只做一件事。這樣每段邏輯才好懂、好改,也才方便後續各自提煉成獨立函式。先別擔心效能——清晰優先,真有效能問題再另外處理。

小步安全進行:

  1. 複製整個迴圈一份,緊接在原迴圈之後。
  2. 從兩個迴圈裡各自刪掉「不屬於它」的那部分:第一個只留 A 行為,第二個只留 B 行為。
  3. 執行測試。
  4. 考慮對每個拆好的迴圈做提煉函式,讓它成為一個有名字的操作。

★ before / after(Go)

// before:一個迴圈同時算兩件不相關的事
youngest := people[0].Age
totalSalary := 0
for _, p := range people {
    if p.Age < youngest { youngest = p.Age }
    totalSalary += p.Salary            // ★ 和上一行毫無關係
}
return fmt.Sprintf("youngest: %d, total: %d", youngest, totalSalary)
 
// after:一個迴圈只做一件事
youngest := people[0].Age
for _, p := range people {
    if p.Age < youngest { youngest = p.Age }
}
totalSalary := 0
for _, p := range people {
    totalSalary += p.Salary
}

★★ 這個手法的重點:它本身沒有價值,它是為了下一步

拆完看起來只是「多跑一次迴圈」。價值在於現在每個迴圈都能被 提煉函式 抽走:

func youngestAge(people []Person) int { … }
func totalSalary(people []Person) int { … }
 
return fmt.Sprintf("youngest: %d, total: %d", youngestAge(people), totalSalary(people))

★ 只拆不提煉 = 白做。 Fowler 明說這個手法「幾乎總是接著做提煉函式」。
拆完就停在那裡,你得到的是「更長的程式碼 + 多一次遍歷」,沒有任何好處。

★ 效能:多跑一次迴圈到底要不要緊

情況判斷
n 小(幾百、幾千)完全無感。兩次 O(n) 還是 O(n),而且資料還在 L1/L2 cache
n 很大、元素很大★ 可能真的變慢——第二次遍歷時資料已被逐出 cache
迴圈體裡有 I/O★★ 這才是真問題:兩次迴圈 = 兩倍的 DB/HTTP 呼叫
// ★★ 絕對不要這樣拆
for _, id := range ids { a[id] = db.Load(id).Name }
for _, id := range ids { b[id] = db.Load(id).Email }   // ★ DB 被打兩次

→ 迴圈體有副作用或 I/O 時,這個手法不適用。 先把 I/O 拉出迴圈(拆分階段),再拆。

★ 判準:先拆求清楚,profile 說慢了再合回去。 合回去很容易,而「一開始就寫成一坨」則很難拆。

Go 的兩個細節

  • range 在迴圈開始時就取好 len(對 slice),所以迴圈內 append 不會造成無限迴圈;但兩個迴圈各自 range 一次是獨立的快照語義。
  • Go 1.22 起迴圈變數每輪新建。拆分後若有 closure 捕獲 p,1.22 前後行為不同——go.mod 的 go 指令版本決定用哪種語義。

TypeScript

拆分迴圈在 TS 幾乎總是通往 map/filter/reduce,成本比 Go 低得多:

const youngest = Math.min(...people.map(p => p.age));
const totalSalary = people.reduce((s, p) => s + p.salary, 0);

⚠️ Math.min(...arr) 在 arr 很大時會爆 call stack(參數個數上限,約幾萬到十萬級,依引擎而定)。大陣列要用 reduce。

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • 拆分會多跑一次集合,理論上較慢;但多數情況差異可忽略,需要時再優化。

  • 兩件事若彼此依賴(後者要用到前者的中間結果),不能單純拆開。

  • 拆分本身常只是中繼步驟,真正的收穫是拆完後的提煉;別拆完就停手。

  • ★ 拆完沒有接著 提煉函式,這次重構就是負收益。

  • ★ 迴圈體有 I/O 或副作用時不適用——拆開就變成兩倍的外部呼叫。

  • 累加變數不能靠 以查詢取代暫時變數 消掉,它必須先被拆到自己的迴圈裡。

  • 提早 break / return 的迴圈不能直接拆:兩個職責的中斷條件不同,拆完語義就變了。先確認中斷條件只服務其中一件事。

  • 迴圈間有順序依賴就不能拆(第二件事需要第一件事的結果)。

🔗 相關工具