控制流程
控制流程(也称为流程控制)是电脑运算领域的用语,意指在程式执行时,个别的指令(或是陈述、子程序)执行或求值的顺序。不论是在宣告式程式语言或是函数程式语言中,都有类似的概念。
在宣告式的程式语言中,流程控制指令是指会改变程式执行顺序的指令,可能是执行不同位置的指令,或是在二段(或多段)程式中选择一个执行。
不同的程式语言所提供的流程控制指令也会随之不同,但一般可以分为以下四种:
- 继续执行位在不同位置的一段指令(无条件分支指令)。
- 若特定条件成立时,执行一段指令,例如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语言几乎都不支援整数的标记。
Goto
[编辑]goto 指令(来自英文go和to的组合)是无条件流程控制指令中最基本的型式。一般在程式中会用以下的方式出现(指令大小写可能会依程式语言而不同)
goto label
goto 指令的效果是调整程式的控制流程,后续就执行标记位置的程式。
goto 指令是许多的计算机科学家视为有害的指令,像荷兰计算机科学家艾兹格·迪科斯彻(Edsger Wybe Dijkstra)就提出了goto有害论[1]。
子程序
[编辑]子程序(subroutine)可以用许多不同的术语来表示,例如程序、函数(尤其是有传回值时)或是方法(特别是子程序属于一个类的一部份)等。
子程序是是完成一项特定工作的代码序列,其他程式可以将流程移转到子程序中,执行特定工作后再回到原来的程式,若程式中有许多部份都需要执行一特定工作,利用子程序的方式可以利用一段程式达到上述的功能,可以减少程式码的长度。
如今子程序也常用来使得程式更加的结构化,例如可以将一些特殊的演算法或特殊的资料存取方式放在子程序中,和其他代码隔离。子程序也是程式模组的一种,若许多程式设计师共同开发一个程式,子程序也有助于其工作的分割及分工。
最简结构化控制流程
[编辑]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. Go To Statement Considered Harmful (PDF). Communications of the ACM. March 1968, 11 (3): 147–148 [2011-02-23]. doi:10.1145/362929.362947. (原始内容存档 (PDF)于2014-05-13).
- ^ Böhm, Jacopini. "Flow diagrams, turing machines and languages with only two formation rules" Comm. ACM, 9(5):366-371, May 1966.
- ^ 施威铭. TURBO C語言實務. 台北: 旗标出版社. 79: p274. ISBN 957717017X.