除了最單純的循序執行之外,所有控制結構都仰賴布林運算式(Boolean Expressions)的求值結果。本章探討布林運算式的撰寫技巧、複合述句與空述句的使用、深層巢狀結構的處理、結構化程式設計的理論基礎,以及控制結構與程式複雜度之間的關係。
19.1 布林運算式#
使用 true / false 而非數值#
在布林測試中應使用 true 和 false 識別字,而非 0 和 1。使用數值旗標的問題在於:讀者無法從程式碼本身判斷 1 代表的是「真」、「假」,還是某個列舉值。大多數現代語言都有內建的布林型別,應善加利用。
- 隱式比較:寫
while ( !done )而非while ( done == false ),減少讀者需要記住的項目數量,讀起來也更接近自然語言
簡化複雜的布林運算式#
- 拆解為中間布林變數:將龐大的測試拆成帶有語意名稱的局部變數,讓每個子運算式各自表達一個概念
- 抽取為布林函式:即使只用一次,將測試移到具有良好命名的函式中,能在程式碼層面引入抽象概念,比註解更可靠
- 使用決策表(Decision Tables):當條件涉及多個變數時,以資料結構取代複雜的 if/case 邏輯,減少控制結構的錯誤機會
正面表述布林運算式#
多重否定會嚴重損害可讀性。改善策略包括:
- 將否定條件轉為肯定條件,並交換 if/else 區塊的程式碼
- 選擇能反轉真值的變數名稱(例如用
errorDetected取代!statusOK) - 套用**乘法定理(DeMorgan’s Theorems)**來化簡帶有否定的布林運算式
DeMorgan’s Theorems 的核心操作:對每個運算元取反、交換
and與or、再對整個運算式取反。例如!a || !b等價於!(a && b)。
括號、求值順序與數線排列#
- 充分使用括號:不要依賴語言的運算子優先順序,括號能同時提升可讀性和正確性
- 數線排列法(Number-Line Order):將數值比較按照數線從小到大排列。例如
MIN <= i && i <= MAX表示 i 在範圍內;i < MIN || MAX < i表示 i 在範圍外 - 與 0 的比較準則:布林變數用隱式比較;數值用
balance != 0;C 的字元指標用*charPtr != '\0';指標用bufferPtr != NULL
短路求值與語言特性#
許多語言(C++、Java 的 &&/||)採用短路求值(Short-Circuit Evaluation):若第一個運算元已能決定結果,就不會評估第二個。這在防止除零錯誤或陣列越界時很有用,但也隱藏了運算順序的依賴。
Java 的
&和|(邏輯運算子)會完整評估所有運算元,不具備短路行為。與&&/||(條件運算子)的行為不同,使用時需特別注意。
常見問題#
- 在 C 系語言中,可將常數放在比較的左側(如
if ( MIN == i ))來防止把==誤寫成= - 在 Java 中,比較物件值應使用
a.equals(b)而非a == b(後者比較的是參考)
19.2 複合述句(述句區塊)#
**複合述句(Compound Statement)**是用大括號 {} 包裹起來、作為單一述句處理的一組述句。撰寫準則:
- 成對撰寫大括號:先寫
for (...) { },再填入內容,就不會遇到大括號不匹配的問題 - 即使只有一行也加大括號:單行述句在維護時容易擴展為多行,若缺少大括號就容易出錯
19.3 空述句#
空述句(Null Statement)是僅由分號組成的述句。處理準則:
- 將分號獨立放一行並縮排,或使用空大括號
{}強調 - 定義
DoNothing()巨集或行內函式,明確表達「此處故意不做任何事」 - 考慮將副作用從迴圈控制移至迴圈本體,改用非空迴圈以提升可讀性
19.4 馴服危險的深層巢狀結構#
研究顯示,多數人無法理解超過三層的巢狀 if。深層巢狀直接違反「管理複雜度」這項軟體首要技術命題。
降低巢狀深度的技巧#
點擊展開:降低巢狀深度的完整技巧列表
- 重新測試部分條件:將深層巢狀拆成多段程式碼,每段用更完整但較扁平的條件測試,以空間換取可讀性
- 使用 break 區塊:以
do { ... } while (false)搭配break建立一段可中途跳出的區塊(需團隊共識才適合使用) - 轉換為 if-then-else 鏈:將巢狀 if 改寫為扁平的 if/else if/else 結構,消除冗餘測試
- 轉換為 case 述句:適用於整數型別的多路分支,可讀性極高
- 將深層巢狀抽取為獨立函式:把迴圈內的分支邏輯移入專屬函式,主迴圈只呈現分派邏輯
- 使用物件導向多型:建立抽象基底類別與子類別,利用多型方法取代 case/if 分支;進一步可搭配工廠方法(Factory Method)集中建立物件
- 使用狀態變數(見 17.3 節)
- **使用守衛子句(Guard Clauses)**提前返回(見 17.1 節)
- 使用例外處理(見 8.4 節)
- 徹底重新設計:深層巢狀可能是對問題理解不足的訊號
mindmap
root((降低巢狀深度))
重構手法
抽取為獨立函式
徹底重新設計
語言機制
break 區塊
case 述句
例外處理
物件導向多型
模式技巧
守衛子句提前返回
狀態變數
if-then-else 鏈
重新測試部分條件巢狀深度過深不代表必須修改,但你應該有充分的理由來解釋為何不做修改。
19.5 程式設計基礎:結構化程式設計#
**結構化程式設計(Structured Programming)源自 Dijkstra 於 1969 年的經典論文。其核心思想是程式應僅使用單一進入、單一退出(Single-Entry, Single-Exit)**的控制結構。
結構化程式設計由三種基本構件組成:
- 循序(Sequence):述句依序執行,如賦值與函式呼叫
- 選擇(Selection):依條件選擇執行路徑,如 if-then-else 與 case 述句
- 迭代(Iteration):重複執行一組述句,如 for、while 迴圈
flowchart LR
subgraph S1["循序"]
direction LR
A1["A"] --> A2["B"] --> A3["C"]
end
subgraph S2["選擇"]
direction TB
B1{"條件"} -- "真" --> B2["路徑 A"]
B1 -- "假" --> B3["路徑 B"]
B2 --> B4["合流"]
B3 --> B4
end
subgraph S3["迭代"]
direction TB
C1["進入迴圈"] --> C2{"條件成立?"}
C2 -- "是" --> C3["迴圈體"]
C3 --> C2
C2 -- "否" --> C4["離開迴圈"]
endBohm-Jacopini 定理(1966)證明了任何控制流程都能僅用循序、選擇、迭代三種結構來建構。
break、continue、return、throw-catch等結構雖然增加了便利性,但應以批判的眼光審視其使用。
19.6 控制結構與複雜度#
控制結構是程式整體複雜度的主要貢獻者。良好的使用方式能降低複雜度,反之則會大幅提升。
McCabe 的循環複雜度#
Tom McCabe 提出的**循環複雜度(Cyclomatic Complexity)**以「決策點」的數量衡量函式的複雜度:
- 從 1 開始計算(代表函式的直通路徑)
- 每遇到
if、while、repeat、for、and、or各加 1 - case 述句中每個分支加 1
判讀基準:
- 0-5:函式大致沒問題
- 6-10:應考慮簡化
- 10 以上:應將部分邏輯抽取到獨立函式中
flowchart TD
Start["從 1 開始計算"] --> Step1["遇到 if / while / for / and / or → 各 +1"]
Step1 --> Step2["遇到 case 述句 → 每個分支 +1"]
Step2 --> Result{"計算結果"}
Result -- "0 ~ 5" --> R1["沒問題"]
Result -- "6 ~ 10" --> R2["應考慮簡化"]
Result -- "10 以上" --> R3["應拆分為多個函式"]
style R1 fill:#2d6a4f,color:#fff
style R2 fill:#e9c46a,color:#000
style R3 fill:#e76f51,color:#fff決策點上限 10 並非鐵律。含有多個 case 的 switch 述句可能超過此值但仍合理,重點在於降低任何時刻需要同時理解的項目數量。
其他複雜度指標#
除了 McCabe 指標之外,還有許多面向能衡量複雜度:使用的資料量、巢狀層數、程式行數、變數的跨距(Span)與存活時間(Live Time)、輸入輸出量等。有些研究者也開發了結合多種指標的複合度量。
要點#
- 撰寫簡潔且可讀的布林運算式,對程式碼品質有顯著貢獻
- 深層巢狀使函式難以理解,但有多種技巧可以相對容易地避免
- 結構化程式設計的核心概念至今仍然適用:任何程式都能由循序、選擇、迭代三種結構組合而成
- 最小化複雜度是撰寫高品質程式碼的關鍵