控制流程
從食譜到 CPU 管線:循序、選擇、重複三種積木,如何在邏輯正確與機器效率之間取得平衡
為什麼食譜不只是「材料清單」
想像你照著食譜煮一鍋咖哩。如果食譜只列出「洋蔥、咖哩塊、馬鈴薯、水」,你會一頭霧水:要先做什麼?水滾了沒?菜軟了嗎?真正有用的食譜會這樣寫:「先把油熱了;如果洋蔥變透明,就下肉;重複翻炒到表面變色;接著加水煮到馬鈴薯能用筷子戳穿為止。」
你發現了嗎?食譜的價值不在「有哪些材料」,而在「以什麼順序、在什麼條件下、做幾次」。程式也是一樣。程式碼裡的每一行指令就像材料,而真正讓程式「活起來」、能依情況做出不同反應的,是控制流程(control flow)——它決定電腦執行指令的順序、分岔與重複。掌握控制流程,等於拿到了讓電腦「照你的意思行動」的方向盤。

三大基本結構:循序、選擇、重複
控制流程聽起來複雜,但其實所有程式的流程都可以拆解成三種基本積木:
- 循序(sequence):由上往下,一行接一行執行。這是預設行為,最直覺。
- 選擇(selection):依條件決定要走哪一條路,也就是分支(branch)。
- 重複(repetition):在某條件成立時,反覆執行同一段程式,也就是迴圈(loop)。
這三種結構看似簡單,卻足以表達任何可被計算的演算法。這不是隨口說說,而是一條有名字、有證明的定理,我們在文末的研究所視角會回頭談它。先讓我們把每一塊積木拿在手上看清楚。
選擇:讓程式學會「看情況」
選擇結構的核心是 if/else。它讓程式根據一個布林(boolean)條件——也就是只會是「真(True)」或「假(False)」的判斷——決定要不要執行某段程式。
score = 72
if score >= 60:
print("及格")
else:
print("不及格")
當條件不只兩種可能時,可以串接多個判斷。Python 用 elif,許多語言用 else if:
if score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
elif score >= 60:
grade = "C"
else:
grade = "F"
這裡有個重要觀念:條件是由上往下逐一檢查的,一旦某個條件成立,後面的 elif 就不再檢查。所以條件的順序會影響結果。如果你把 score >= 60 放在最前面,那麼 95 分也只會拿到 C,因為它「先撞上」了 60 分這道門。
重複:把重複的勞動交給電腦
電腦最擅長的就是不厭其煩地做同一件事。重複結構分成兩大類:
while迴圈:當條件成立時持續執行,適合「不知道要做幾次、只知道何時該停」的情境。for迴圈:走訪一個範圍或一組資料,適合「知道要做幾次」的情境。
# while:印出小於 100 的 2 的次方
n = 1
while n < 100:
print(n)
n = n * 2
# for:計算 1 到 10 的總和
total = 0
for i in range(1, 11):
total = total + i
print(total) # 55
寫 while 迴圈時最常見的陷阱是無窮迴圈(infinite loop):如果條件永遠為真,程式就會卡死。上面的 while 範例之所以會停,是因為每一輪都讓 n 變大,終究會超過 100。請務必確認迴圈內有「朝向終止條件前進」的動作。
兩個常用的流程控制關鍵字也值得認識:break 會立刻跳出整個迴圈,continue 則跳過本輪剩下的程式、直接進入下一輪。
巢狀與短路:細節裡的魔鬼
巢狀(nesting)
結構可以互相包覆。迴圈裡放迴圈、判斷裡放判斷,就形成巢狀。例如印出九九乘法表,就是一個迴圈套另一個迴圈:
for i in range(1, 10):
for j in range(1, 10):
print(f"{i}×{j}={i*j}", end="\t")
print() # 每一列結束換行
巢狀很強大,但層數一多就難讀。一般建議巢狀不要超過三層;過深時,可以把內層邏輯抽成函式(function),讓程式回到「淺而清楚」的狀態。
短路求值(short-circuit evaluation)
當條件用 and、or 串接多個判斷時,許多語言採用短路求值:只要結果已經確定,就不再評估後面的條件。
# 若 user 是 None,user.is_admin 會出錯。
# 但短路求值讓左邊先擋下來:
if user is not None and user.is_admin:
grant_access()
在 and 中,只要左邊為假,整個結果必為假,右邊就被「短路」跳過;在 or 中,只要左邊為真,右邊也被跳過。這不只是效能優化,更是一種保護:上面的例子若沒有短路,當 user 為 None 時讀取 user.is_admin 就會崩潰。善用短路,能寫出既安全又簡潔的條件。
動手看一個例子:判斷閏年
讓我們用一個經典問題,把選擇與巢狀條件整合起來:判斷某一年是不是閏年(leap year)。
閏年規則是:
- 能被 4 整除的年份是閏年,
- 但能被 100 整除的不是閏年,
- 除非它同時能被 400 整除,那又是閏年。
我們先用逐步推理走一遍 2000 年與 1900 年:
| 年份 | 能被 4 整除? | 能被 100 整除? | 能被 400 整除? | 結論 |
|---|---|---|---|---|
| 2000 | 是 | 是 | 是 | 閏年 |
| 1900 | 是 | 是 | 否 | 平年 |
| 2024 | 是 | 否 | — | 閏年 |
| 2023 | 否 | — | — | 平年 |
把規則翻成程式,有兩種寫法。第一種用巢狀 if,貼近上面的「先判斷、再例外」的思路:
def is_leap(year):
if year % 4 == 0:
if year % 100 == 0:
if year % 400 == 0:
return True # 能被 400 整除,是閏年
else:
return False # 能被 100 但不能被 400,不是
else:
return True # 能被 4 但不能被 100,是
else:
return False # 不能被 4,不是
第二種用布林運算把三層條件壓成一行,配合短路求值,可讀性反而更高:
def is_leap(year):
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
兩種寫法邏輯完全相同,但後者展示了一個重要觀念:清楚的條件運算式往往勝過深層巢狀。能用一個表達意圖的布林式說清楚的事,就不必堆疊多層 if。
重點回顧
- 控制流程決定指令的執行順序、分岔與重複,是程式從「材料清單」變成「會做事的食譜」的關鍵。
- 三大基本結構是循序、選擇、重複;任何演算法都能由它們組合而成。
if/elif/else的條件由上往下逐一檢查,順序會影響結果;while適合「不知次數」、for適合「已知次數」。- 巢狀讓結構可以互相包覆但層數宜淺;短路求值讓
and/or在結果確定時停止評估,兼具效能與安全。 - 善用清楚的布林運算式,常能取代深層巢狀,寫出更易讀的程式。
深入探討(研究所視角)
結構化程式定理:為什麼三種積木就夠了
控制流程的三大結構並非工程慣例,而有堅實的理論根基。Böhm–Jacopini 定理(又稱結構化程式定理,1966)證明:任何可計算的函式,都可以僅用循序、選擇、重複三種控制結構組合的流程圖來表達——不需要任意跳躍。這條定理是 1970 年代「結構化程式設計(structured programming)」運動的理論支柱,也是我們今天能安心宣稱「不用 goto 也能寫出任何程式」的依據。
這也解釋了為何要避免 goto。goto 允許程式從任意一點跳到另一任意一點,會編織出 Dijkstra 在著名文章〈Go To Statement Considered Harmful〉(1968) 中批評的「義大利麵程式(spaghetti code)」:控制流糾纏難解,程式的靜態文字與動態執行軌跡嚴重脫節,使人難以推理其正確性。結構化程式的價值在於,每個區塊都有單一入口、單一出口,讓「閱讀順序」貼近「執行順序」,大幅降低理解與驗證的認知負擔。值得補充的是,現代語言保留的 break、continue、提早 return、以及例外處理(exception),可視為受嚴格約束的結構化跳躍——它們在不破壞單一入口的前提下,提供有限而可預測的「出口」,這正是純粹理論與工程實務之間的折衷。
分支預測:抽象之下的硬體真相
到目前為止我們把控制流程當成純粹的邏輯,但在 CPU 內部,分支其實有實打實的效能代價。現代處理器採用深度管線(pipeline):一條指令的取得、解碼、執行被切成多個階段,多條指令在不同階段同時推進,藉此提高吞吐量。問題是,遇到一個 if,CPU 在條件算完之前,並不知道下一步該抓哪段指令。
為了不讓管線停擺,CPU 使用分支預測器(branch predictor):根據歷史紀錄「猜」分支會往哪走,並先把猜中的路徑灌進管線。猜對了,幾乎沒有額外成本;猜錯了,已經進入管線的指令必須全部清空、從正確位置重抓,這個分支誤判懲罰(branch misprediction penalty) 在現代 CPU 上往往達到十幾到二十多個時脈週期。
這帶來一個對研究生很重要的洞見:分支的「可預測性」會直接影響效能。考慮對一個陣列做條件累加:
# 若資料「已排序」,分支高度規律,預測器幾乎全中
# 若資料「隨機」,分支忽真忽假,預測器頻頻誤判
total = 0
for x in data:
if x > threshold:
total += x
在編譯式語言(如 C/C++)中,同樣這段程式,對已排序資料的執行速度可能比對隨機資料快上數倍——程式碼一字未改,差別純粹來自分支是否容易被預測。這也是為什麼高效能程式設計會出現「無分支程式設計(branchless programming)」技巧:用位元運算或條件搬移指令(conditional move)取代 if,把資料相依的分支轉成資料相依的算術,藉此完全繞過預測器。
我們可以把分支的期望成本粗略寫成:
$$ C_{\text{branch}} \approx p_{\text{miss}} \cdot P_{\text{penalty}} $$
其中 $p_{\text{miss}}$ 是誤判機率,$P_{\text{penalty}}$ 是每次誤判的週期懲罰。當 $p_{\text{miss}} \to 0$(分支高度規律),分支幾乎免費;當 $p_{\text{miss}} \to 0.5$(純隨機,最難猜),成本達到最大。這也呼應了演算法複雜度分析的一個盲點:兩段同為 $O(n)$ 的程式,實際執行時間可能因為快取行為與分支可預測性而相差數倍。漸近複雜度告訴你「規模如何增長」,但常數因子裡藏著硬體的真相。
把兩端連起來
從 Böhm–Jacopini 的理論到分支預測的硬體,控制流程橫跨了計算機科學的兩個極端。上層,它是讓我們能形式化推理程式正確性的抽象結構——這條路通往程式驗證、靜態分析與型別理論。下層,它是 CPU 必須實際押注、可能押錯的物理事件——這條路通往計算機結構、編譯器最佳化與效能工程。一個 if,同時活在這兩個世界裡。理解控制流程,不只是學會寫 if 和 while,更是學會在「邏輯正確」與「機器效率」之間,看見那條始終存在的張力。