控制流

**控制流程**（也稱為**流程控制**）是電腦運算領域的用語，意指在程式執行時，個別的指令（或是陳述、子程式）執行或求值的順序。不論是在宣告式程式語言或是函式程式語言中，都有類似的概念。

在宣告式的程式語言中，**流程控制指令**是指會改變程式執行順序的指令，可能是執行不同位置的指令，或是在二段（或多段）程式中選擇一個執行。

不同的程式語言所提供的流程控制指令也會隨之不同，但一般可以分為以下四種：

* 繼續執行位在不同位置的一段指令（無條件分支指令）。
* 若特定條件成立時，執行一段指令，例如 C 語言的 switch 指令，是一種有條件分支指令。
* 執行一段指令若干次，直到特定條件成立為止，例如 C 語言的 for 指令，仍然可視為一種有條件分支指令。
* 執行位在不同位置的一段指令，但完成後會繼續執行原來要執行的指令，包括子程式、協程（coroutine）及計算續體（continuation）。
* 停止程式，不執行任何指令（無條件的終止）。

中斷以及 Unix 系統中的訊號等較低階的機制也可以造成類似子程式的效果，不過通常這類機制會用來回應外部的事件或是輸入。程式自修改因為其對程式碼的影響，也會影響控制流程，但多半不會有明顯的流程控制指令。

在機器語言或組合語言中，流程控制是藉由修改程式計數器數值來達到。一些中央處理器只支援條件分支（branch）或是無條件分支（有時會稱為 jump）。

## 基本概念

### 標記

標記是一個標示在原始碼固定位置中的名稱或數字，其他位置的流程控制指令可以參考標記的位置，執行標記位置所對應的程式。標記本身不影響程式的進行，除了標示位置外，對程式執行沒有其他的作用。

有一些程式語言（像 Fortran 及 BASIC 等）利用行號作為標記。行號是標示在每一行程式最前面的自然數，不一定要是連續的數字，在不受流程控制指令影響的情形下，程式會從最小的行號依序執行，而流程控制指令需指定對應的行號。以下是一個 BASIC 的例子:

在像是 C 及 Ada 等程式語言中，標記是一個標識符，一般出現在一行的最前面，後面會加一個冒號作為識別，以下是 C 語言的例子：

Algol 60 語言同時支援整數（類似行號）及標識符的標記（二者後面都要加上冒號），不過其他 Algol 語言幾乎都不支援整數的標記。

### Goto

goto 指令（來自英文 go 和 to 的組合）是無條件流程控制指令中最基本的型式。一般在程式中會用以下的方式出現（指令大小寫可能會依程式語言而不同）

```
   **goto** _label_
```

goto 指令的效果是調整程式的控制流程，後續就執行標記位置的程式。

goto 指令是許多的電腦科學家視為有害的指令，像荷蘭電腦科學家艾茲格・迪科斯徹（Edsger Wybe Dijkstra）就提出了 goto 有害論。

### 子程式

子程式（subroutine）可以用許多不同的術語來表示，例如程式、函式（尤其是有傳回值時）或是方法（特別是子程式屬於一個類的一部份）等。

子程式是是完成一項特定工作的代碼序列，其他程式可以將流程移轉到子程式中，執行特定工作後再回到原來的程式，若程式中有許多部份都需要執行一特定工作，利用子程式的方式可以利用一段程式達到上述的功能，可以減少程式碼的長度。

如今子程式也常用來使得程式更加的結構化，例如可以將一些特殊的演算法或特殊的資料存取方式放在子程式中，和其他代碼隔離。子程式也是程式模組的一種，若許多程式設計師共同開發一個程式，子程式也有助於其工作的分割及分工。

## 最簡結構化控制流程

1966 年 5 月 Corrado Böhm 及 Giuseppe Jacopini 在《Communications of the ACM》期刊發表論文，說明任何一個有 goto 指令的程式，可以改為完全不使用 goto 指令的程式，goto 指令可以用選擇指令（IF THEN ELSE）及迴圈（WHILE 特定條件 DO 特定程式）取代，可能會再多一些重覆的程式碼及額外的布林變數。後來的研究者已證明選擇指令也可以用迴圈取代，不過需要更多的布林變數。Böhm 及 Jacopini 的論文說明程式可以完全不使用 goto，但是在實務上大家不一定會想要這麼進行。

其他的研究說明若控制結構只有一個進入點（entry）及一個結束點（exit），這樣的程式會比其他型式的程式容易理解。因此這樣的程式可以像一個指令一樣放在程式的任何部份，不必擔心會破壞其結構，換句話說，這種程式是「可組成的」（composable）。

## 常用的控制結構

若一程式語言支援控制結構，控制結構開始時多半都會有特定的關鍵字，以標明是使用哪一種控制結構。但只有部份程式語言在控制結構結束時會有特定的關鍵字表示結束，因此可以依控制結構結束時是否有特定關鍵字來將程式語言分為二類。

* 沒有特定關鍵字的語言：Algol 60、C、C++、Haskell、Java、Pascal、Perl、PHP、PL/I、Python、Windows PowerShell。這類語言需要有關鍵字可以將 group 程式指令 together：
  * Algol 60 及 Pascal：`begin` ... `end`
  * C, C++, Java, Perl, PHP, and PowerShell：利用大括號 `{` ... `}`
  * PL/1：`DO` ... `END`
  * Python：利用縮排的層次，詳細內容請參考 Off-side 規則
  * Haskell：可以利用縮排或大括號，兩者可以混用
* 有特定關鍵字的語言：Ada、Algol 68、 Modula-2、Fortran 77、Visual Basic，使用的特定關鍵字依程式語言而不同：
  * Ada: 其關鍵字為 `end` + _space_ + 啟始控制結構的關鍵字，如 `if` ... `end if`, `loop` ... `end loop`
  * Algol 68, Bourne shell, Mythryl：將啟始關鍵字反寫，如 `if` ... `fi`, `case` ... `esac`
  * Fortran 77: 其關鍵字為 `end` + initial keyword，如 `IF` ... `ENDIF`, `DO` ... `ENDDO`
  * Modula-2: 不論何種控制結構，其關鍵字均為 `END`
  * Visual Basic: 每種控制結構均有各自的結尾關鍵字，如 `If` ... `End If`; `For` ... `Next`; `Do` ... `Loop`
* 沒有任何特徵的語言:logo

## 條件判斷

條件判斷是依指定變數或運算式的結果，決定後續執行的程式，最常用的是 if-else 指令，可以根據指定條件是否成立，決定後續的程式。也可以組合多個 if-else 指令，進行較複雜的條件判斷。
許多程式語言也提供多選一的條件判斷，例如 C 語言的 switch-case 指令。

## 迴圈

迴圈是指一段在程式中只出現一次，但可能會連續執行多次的程式碼。常見的迴圈可以分為二種，指定執行次數的迴圈（如 C 語言的 for 迴圈）以及指定繼續執行條件（或停止條件）的迴圈（如 C 語言的 while 迴圈）。

在一些函式程式語言（例如 Haskell 和 Scheme）中會使用遞迴或不動點組合子來達到迴圈的效果，其中尾端遞迴是一種特別的遞迴，很容易轉換為迭代。

## 結構化的非區部控制流程

有些程式語言會提供非區部的控制流程（non-local control flow），會允許流程跳出目前的程式碼，進入一段事先指定的程式碼。常用的結構化非區部控制流程可分為條件處理、例外處理及計算續體（Continuation）三種。

### 條件處理

PL/I 程式語言中有 22 種標準的條件（如 ZERODIVIDE SUBSCRIPTRANGE ENDFILE），可以在程式中設定，當特定條件成立時需進行的指令，程式設計者也可以定義自己的條件，並在程式中使用。

條件成立時，只能設定一個需進行的指令（類似未結構化的 if 指令），大部份的應用中，都會指定執行 goto 指令，跳到其他程式碼執行對應的流程。

不過因為有些條件處理的實現會增加許多程式碼及執行時間（特別 SUBSCRIPTRANGE），所以許多程式設計者會儘量不使用條件處理。

條件處理的語法如下:

```
 **ON** _條件_ **GOTO** _label_
```

### 異常處理

有些程式語言可提供不需要使用 `GOTO` 的結構化異常處理程式：

在 `try{...}` 的區塊中，若有異常情形時，程式就會離開 try 的區塊，由後續的一個或多個 `catch` 子句判斷需執行何種異常處理。在 D、Java、C# 及 Python 程式語言中，`try{...}` 區塊中還可以加入一個 `finally` 子句，不管程式流程是否離開 `try{...}` 區塊，`finally` 子句中的程式都一定會執行，常用在當程式結束處理時，需要放棄一些外部資源（檔案或資料庫連結）的情形下：

由於上述情形相當普遍，C# 提供一種特殊的語法進行相同的處理：

只要離開 `using` 區塊，編譯器會自動釋放 `stm` 物件，Python 的 `with` 指令及也有類似的功能。

這些語言都有定義標準的異常情形及其出現的條件，程式設計者也可以丟出自己產生的異常（其實 C++ 及 Python 的 throw 和 catch 支援絕大多數形態的物件）。

若某一個 `throw` 指令找不到對應的 `catch`，控制流程會離開目前的副程式或控制結構，設法找到對應的 `catch`，若到主程式的結尾還是找不到對應的 `catch`，程式會強制結束，並顯示適當的錯誤訊息。

AppleScript 手稿語言可以將 "`try`" 區塊分為幾個部份，提供不同的訊息及異常：

### 續體

續體（Continuation）實化了延續性，它可以將目前子程式的執行狀態（包括目前的堆疊，區部變數及執行到的位置）儲存成一個物件，後續在其他子程式中可以利用此物件回到此子程式現在的執行狀態。

續體一詞也可以指第一類續體（first-class continuation），是指程式語言可以在任意時間點儲存目前的執行狀態，並在之後回到之前儲存的執行狀態。

程式需要分配空間給子程式用到的區域變數，而且在子程式結束時需釋出這些變數用到的空間。許多程式語言利用調用堆疊來存放這些變數，可以簡單快速的分配及釋出空間。也有一些程式語言使用動態記憶體分配來儲存變數，可以較靈活的分配變數，但分配及釋出空間較不方便。這二個架構下續體的處理方式也會不同，各有其優點及缺點。

Scheme 語言利用 `call-with-current-continuation` 函式（縮寫為 call/cc） 可提供續體功能。

## 參考資料

## 相關條目

* 分支 (計算機科學)
* 控制流圖
* 控制流程圖
* 控制表
* 協程
* 循環複雜度
* 流程圖
* Goto
* 事件迴圈
* 遞迴 (電腦科學)
* 麵條式代碼
* 結構化程式設計
* 子程式
* Switch 指令