有些控制結構處於「前衛」與「被否定」之間的灰色地帶,並非所有語言都提供,但在謹慎使用下可帶來價值。本章討論多處回傳(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 的場景——需要在偵測到錯誤時跳到統一的清理區段。書中比較了四種替代方案:
點擊展開:四種錯誤處理方案比較
- goto 方案:每個錯誤點直接
goto到結尾的清理標籤。避免了深層巢狀和多餘測試,但使用了goto。 - 巢狀 if 方案:用巢狀
if-else確保只在前一步驟成功時執行下一步。避免了goto,但巢狀極深,錯誤處理程式碼與觸發條件距離過遠。 - 狀態變數方案:用狀態變數記錄是否處於錯誤狀態,每一步驟前先檢查。避免了
goto和深層巢狀,但引入額外測試,且此模式不夠常見,需要充分文件說明。 - 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)。