有些控制結構處於「前衛」與「被否定」之間的灰色地帶,並非所有語言都提供,但在謹慎使用下可帶來價值。本章討論多處回傳(multiple returns)、遞迴(recursion)與 goto,以及如何在它們之間做出取捨。

17.1 子程式中的多處回傳#

大多數語言都支援 return(或等效語法如 VB 的 Exit Sub),允許子程式在中途跳出並將控制權交還給呼叫端。

使用 return 的指導方針#

  • 在能提升可讀性時使用 return:一旦確定答案就立即回傳,避免撰寫多餘的程式碼。例如比較函式在判定大於或小於後直接回傳,比走到最後再統一回傳更清楚。
  • 使用衛述句(guard clauses)簡化錯誤處理:連續檢查多個前置條件時,巢狀 if 會讓縮排層層加深、遮蔽主要邏輯。改用「偵測到錯誤就提前離開」的衛述句寫法,能讓正常路徑更清晰。
  • 盡量減少 return 數量:讀到子程式底部時若不知道上方可能已回傳,會增加理解難度。只在確實提升可讀性時才使用多處回傳。

17.2 遞迴#

遞迴(Recursion) 是指子程式解決問題的一小部分,然後將問題分解為更小的片段,再呼叫自己來處理每個片段。遞迴適合的問題通常具備兩個特徵:小規模情形容易直接解決,大規模情形容易分解。

經典的 QuickSort 是遞迴的良好應用:將陣列分為兩半,分別對兩半遞迴排序,子陣列小到無需排序時即停止。迷宮問題也是典型範例——在每個位置嘗試四個方向,記錄已走過的點以防止無窮遞迴,直到找到出口。

使用遞迴的訣竅#

  • 確保遞迴能停止:必須包含一條不再遞迴的路徑(base case)。
  • 使用安全計數器(safety counter)防止無窮遞迴:若無法設計簡單的終止條件,傳入計數器參數,超過上限時強制停止。計數器應使用類別成員變數或以參數傳遞,不可是區域變數。
  • 將遞迴限制在單一子程式內:循環遞迴(A 呼叫 B 呼叫 C 呼叫 A)極難偵測與理解。
  • 留意堆疊使用量:遞迴的堆疊消耗難以預測,避免在遞迴函式內配置大量區域變數,改用 new 在 heap 上配置。

不要用遞迴計算階乘或費式數列。 這是教科書的壞示範——遞迴版比迭代版更慢、更難懂、記憶體使用不可預測。在使用遞迴之前,先考慮用堆疊加迭代是否能更好地解決問題。

17.3 goto#

goto 的爭論由來已久,但在現代原始碼庫中它仍然存在。而且 goto 辯論的現代變體——多處回傳、多處迴圈跳出、例外處理——至今仍在上演。

反對 goto 的論點#

  • Dijkstra(1968)指出程式碼品質與 goto 使用量成反比,不含 goto 的程式碼更容易被證明正確。
  • goto 使程式碼難以用縮排表現邏輯結構,也會妨礙編譯器最佳化。
  • 一旦引入 goto 就容易蔓延——壞的會隨好的一起出現。

支持 goto 的論點#

  • 支持者主張謹慎使用而非濫用。適當的 goto 可消除重複程式碼,讓資源清理邏輯集中在一處。
  • 消除 goto 不是目標,而是好的分解與控制結構選擇的自然結果

錯誤處理與 goto#

這是經驗豐富的程式設計師最常使用 goto 的場景——需要在偵測到錯誤時跳到統一的清理區段。書中比較了四種替代方案:

點擊展開:四種錯誤處理方案比較
  1. goto 方案:每個錯誤點直接 goto 到結尾的清理標籤。避免了深層巢狀和多餘測試,但使用了 goto
  2. 巢狀 if 方案:用巢狀 if-else 確保只在前一步驟成功時執行下一步。避免了 goto,但巢狀極深,錯誤處理程式碼與觸發條件距離過遠。
  3. 狀態變數方案:用狀態變數記錄是否處於錯誤狀態,每一步驟前先檢查。避免了 goto 和深層巢狀,但引入額外測試,且此模式不夠常見,需要充分文件說明。
  4. try-finally 方案:將操作放在 try 區塊、清理放在 finally 區塊。最簡潔,但前提是語言支援且整個程式碼庫一致使用例外機制。
flowchart TD
    subgraph S1["goto 方案"]
        direction TB
        G1["步驟 1"] --> GE1{"錯誤?"}
        GE1 -- "是" --> GC["goto 清理"]
        GE1 -- "否" --> G2["步驟 2"]
        G2 --> GE2{"錯誤?"}
        GE2 -- "是" --> GC
        GE2 -- "否" --> G3["步驟 3"]
        G3 --> GC["清理與退出"]
    end

    subgraph S2["巢狀 if 方案"]
        direction TB
        N1["步驟 1"] --> NE1{"成功?"}
        NE1 -- "是" --> N2["步驟 2"]
        NE1 -- "否" --> NC["錯誤處理"]
        N2 --> NE2{"成功?"}
        NE2 -- "是" --> N3["步驟 3"]
        NE2 -- "否" --> NC
        N3 --> NC["清理"]
    end

    subgraph S3["狀態變數方案"]
        direction TB
        V1["步驟 1"] --> VC1["更新狀態"]
        VC1 --> VT2{"狀態正常?"}
        VT2 -- "是" --> V2["步驟 2"]
        VT2 -- "否" --> VE["跳過"]
        V2 --> VC2["更新狀態"]
        VC2 --> VT3{"狀態正常?"}
        VT3 -- "是" --> V3["步驟 3"]
        VT3 -- "否" --> VE2["跳過"]
        VE --> VClean["清理"]
        VE2 --> VClean
        V3 --> VClean
    end

    subgraph S4["try-finally 方案"]
        direction TB
        T1["try"] --> TS1["步驟 1"]
        TS1 --> TS2["步驟 2"]
        TS2 --> TS3["步驟 3"]
        TS3 --> TF["finally: 清理"]
        TS1 -. "例外" .-> TF
        TS2 -. "例外" .-> TF
    end

若語言支援 try-finally 且程式碼庫尚未統一使用其他方案,try-finally 是最直觀的選擇。否則,狀態變數方案略優於 goto 和巢狀 if 方案。無論選哪種,關鍵是在整個專案中一致地使用。

goto 使用指導方針摘要#

  • 僅在語言缺乏等效結構化控制結構時,才用 goto 模擬,且要精確模擬。
  • 當語言提供等效內建結構時,不要使用 goto
  • 若為效能而使用 goto,要實際量測並記錄效能提升。
  • 每個子程式最多一個 goto 標籤。
  • 只向前跳、不向後跳。
  • 確保所有 goto 標籤都有被使用,否則刪除。
  • 確保 goto 不會產生無法到達的程式碼。

17.4 針對不常見控制結構的觀點#

軟體開發的進步,在很大程度上來自於限制程式設計師對程式碼的操作自由度。不受限的 goto、動態計算跳躍目標、跨子程式跳躍、自我修改程式碼等做法都曾被視為合理,如今都已被視為危險而過時。作者以懷疑的態度看待本章中的非傳統控制結構,認為其中大多數最終可能走向同樣的歸宿——被淘汰。

更多資源#

  • Fowler, Martin. Refactoring(1999)——在「Replace Nested Conditional with Guard Clauses」重構手法中,Fowler 主張使用多處 return 來降低巢狀、提升清晰度。
  • Dijkstra, Edsger.〈Go To Statement Considered Harmful〉(1968)——引發 goto 論戰的經典文獻。
  • Knuth, Donald.〈Structured Programming with go to Statements〉(1974)——以大量範例說明移除或加入 goto 對效率的影響。
  • Rubin, Frank.〈“GOTO Considered Harmful” Considered Harmful〉(1987)——引發 CACM 史上最多讀者回響的投書。

要點#

  • 多處回傳能提升可讀性並避免深層巢狀,但應謹慎使用。
  • 遞迴為少數問題提供優雅解法,但要確保能終止、注意堆疊、先考慮迭代替代方案。
  • goto 在極少數情況下是最佳選擇(如統一資源清理),但應作為最後手段,且在現代語言中通常有更好的替代方案(如 try-finally)。