控制流程
控制流程(也稱為流程控制)是電腦運算領域的用語,意指在程式執行時,個別的指令(或是陳述、子程序)執行或求值的順序。不論是在宣告式程式語言或是函數程式語言中,都有類似的概念。
在宣告式的程式語言中,流程控制指令是指會改變程式執行順序的指令,可能是執行不同位置的指令,或是在二段(或多段)程式中選擇一個執行。
不同的程式語言所提供的流程控制指令也會隨之不同,但一般可以分為以下四種:
- 繼續執行位在不同位置的一段指令(無條件分支指令)。
- 若特定條件成立時,執行一段指令,例如C語言的switch指令,是一種有條件分支指令。
- 執行一段指令若干次,直到特定條件成立為止,例如C語言的for指令,仍然可視為一種有條件分支指令。
- 執行位在不同位置的一段指令,但完成後會繼續執行原來要執行的指令,包括子程序、协程(coroutine)及计算续体(continuation)。
- 停止程式,不執行任何指令(無條件的終止)。
中斷以及Unix系統中的信号等較低階的機制也可以造成類似子程序的效果,不過通常這類機制會用來回應外部的事件或是輸入。程序自修改因為其對程式碼的影響,也會影響控制流程,但多半不會有明顯的流程控制指令。
在机器语言或汇编语言中,流程控制是藉由修改程式計數器數值來達到。一些中央處理器只支援條件分支(branch)或是無條件分支(有時會稱為jump)。
基本概念
標記
標記是一個標示在源代码固定位置中的名稱或數字,其他位置的流程控制指令可以參考標記的位置,執行標記位置所對應的程式。標記本身不影響程式的進行,除了標示位置外,對程式執行沒有其他的作用。
有一些程式語言(像Fortran及BASIC等)利用行號作為標記。行號是標示在每一行程式最前面的自然數,不一定要是連續的數字,在不受流程控制指令影響的情形下,程式會從最小的行號依序執行,而流程控制指令需指定對應的行號。以下是一個BASIC的例子:
10 LET X = 3
20 PRINT "*"
30 LET X = X - 1
40 IF X > 0 THEN GOTO 20
50 END
在像是C及Ada等程式語言中,標記是一個標識符,一般出現在一行的最前面,後面會加一個冒號作為識別,以下是C語言的例子:
Success: printf ("The operation was successful.\n");
Algol 60語言同時支持整數(類似行號)及標識符的標記(二者後面都要加上冒號),不過其他Algol語言幾乎都不支援整數的標記。
最簡結構化控制流程
1966年5月Corrado Böhm及Giuseppe Jacopini在《Communications of the ACM》期刊發表論文[2],說明任何一個有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:可以利用縮排或大括號,兩者可以混用
- Algol 60及 Pascal:
- 有特定關鍵字的語言: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
- Ada: 其關鍵字為
- 没有任何特征的语言:logo
條件判斷
條件判斷是依指定變數或運算式的結果,決定後續執行的程序,最常用的是if-else指令,可以根據指定條件是否成立,決定後續的程序。也可以組合多個if-else指令,進行較複雜的條件判斷。 許多程式語言也提供多選一的條件判斷,例如C語言的switch-case指令。[3]
迴圈
迴圈是指一段在程式中只出現一次,但可能會連續執行多次的程式碼。常見的迴圈可以分為二種,指定執行次數的迴圈(如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 {
xxx1 // Somewhere in here
xxx2 // use: '''throw''' someValue;
xxx3
} catch (someClass& someId) { // catch value of someClass
actionForSomeClass
} catch (someType& anotherId) { // catch value of someType
actionForSomeType
} catch (...) { // catch anything not already caught
actionForAnythingElse
}
在try{...}
的區塊中,若有異常情形時,程式就會離開try的區塊,由後續的一個或多個catch
子句判斷需執行何種異常處理。在D、Java、C#及Python程式語言中,try{...}
區塊中還可以加入一個finally
子句,不管程式流程是否離開try{...}
區塊,finally
子句中的程式都一定會執行,常用在當程式結束處理時,需要放棄一些外部資源(檔案或資料庫連結)的情形下:
FileStream stm = null; // C# example
try {
stm = new FileStream ("logfile.txt", FileMode.Create);
return ProcessStuff(stm); // may throw an exception
} finally {
if (stm != null)
stm. Close();
}
由於上述情形相當普遍,C#提供一種特殊的語法進行相同的處理:
using (FileStream stm = new FileStream ("logfile.txt", FileMode.Create)) {
return ProcessStuff(stm); // may throw an exception
}
只要離開 using
區塊,編譯器會自動釋放stm
物件,Python的 with
指令及也有類似的功能。
這些語言都有定義標準的異常情形及其出現的條件,程式設計者也可以丟出自己產生的異常(其實C++及Python的throw和catch支援絕大多數形態的物件)。
若某一個throw
指令找不到對應的catch
,控制流程會離開目前的副程式或控制結構,設法找到對應的catch
,若到主程式的結尾還是找不到對應的catch
,程式會強制結束,並顯示適當的錯誤訊息。
AppleScript脚本语言可以將"try
"區塊分為幾個部份,提供不同的訊息及異常:
try
set myNumber to myNumber / 0
on error e number n from f to t partial result pr
if ( e = "Can't divide by zero" ) then display dialog "You idiot!"
end try
延續性
延續性(Continuation)可以將目前子程序的執行狀態(包括目前的堆疊,區部變數及執行到的位置)儲存成一個物件,後續在其他子程序中可以利用此物件回到此子程序現在的執行狀態。
延續性一詞也可以指第一類延續性(first-class continuation),是指程式語言可以在任意時間點儲存目前的執行狀態,並在之後回到之前儲存的執行狀態。
程式需要分配空間給子程序用到的區域變數,而且在子程序結束時需釋出這些變數用到的空間。許多程式語言利用調用堆疊來存放這些變數,可以簡單快速的分配及釋出空間。也有一些程式語言使用動態記憶體分配來儲存變數,可以較靈活的分配變數,但分配及釋出空間較不方便。這二個架構下延續性的處理方式也會不同,各有其優點及缺點。
Scheme語言利用call-with-current-continuation
函數(縮寫為call/cc) 可提供延續性功能。
參考資料
- Edsger Dijkstra. (PDF). Communications of the ACM. March 1968, 11 (3): 147–148. doi:10.1145/362929.362947.
- Böhm, Jacopini. "Flow diagrams, turing machines and languages with only two formation rules" Comm. ACM, 9(5):366-371, May 1966.
- 施威銘. . 台北: 旗標出版社. 79: p274. ISBN 957717017X.