流程控制

流程控制

控制流程(也称为流程控制)是计算机运算领域的用语,意指在程式运行时,个别的指令(或是陈述、子程式)运行或求值的顺序。不论是在声明式程式语言或是函式程式语言中,都有类似的概念。

在声明式的程式语言中,流程控制指令是指会改变程式运行顺序的指令,可能是运行不同位置的指令,或是在二段(或多段)程式中选择一个运行。

    • 中文名:流程控制
    • 别称:控制流程
    • 类别:计算机运算领域的用语
    • 释义:个别的指令运行或求值的顺序

基本概念

在声明式的程式语言中,流程控制指令是指会改变程式运行顺序的指令,可能是运行不同位置的指令,或是在二段(或多段)程式中选择一个运行。

不同的程式语言所提供的流程控制指令也会随之不同,但一般可以分为以下四种:

  • 继续运行位在不同位置的一段指令(无条件分支指令)。

  • 若特定条件成立时,运行一段指令,例如C语言的switch指令,是一种有条件分支指令。

  • 运行一段指令若干次,直到特定条件成立为止,例如C语言的for指令,仍然可视为一种有条件分支指令。

  • 运行位于不同位置的一段指令,但完成后会继续运行原来要运行的指令,包括子程式、协程(coroutine)及延续性(continuation)。

  • 停止程式,不运行任何指令(无条件的终止)。

中断以及Unix系统中的信号等较低级的机制也可以造成类似子程式的效果,不过通常这类机制会用来回应外部的事件或是输入。程式自修改因为其对代码的影响,也会影响控制流程,但多半不会有明显的流程控制指令。

在机器语言或彙编语言中,流程控制是藉由修改程式计数器数值来达到。一些中央处理器只支持条件分支(branch)或是无条件分支(有时会称为jump)。

标记

标记是一个标示在原始码固定位置中的名称或数字,其他位置的流程控制指令可以参考标记的位置,运行标记位置所对应的程式。标记本身不影响程式的进行,除了标示位置外,对程式运行没有其他的作用。

有一些程式语言(像Fortran及BASIC等)利用行号作为标记。行号是标示在每一行程式最前面的自然数,不一定要是连续的数字,在不受流程控制指令影响的情形下,程式会从最小的行号依序运行,而流程控制指令需指定对应的行号。以下是一个BASIC的例子:

LETX=3PRINT*LETX=X-1IFX>0THENGOTO20END

在像是C及Ada等程式语言中,标记是一个标识符,一般出现在一行的最前面,后面会加一个冒号作为识别,以下是C语言的例子:

Success: printf (The operation was successful.\n);

Algol 60语言同时支持整数(类似行号)及标识符的标记(二者后面都要加上冒号),不过其他Algol语言几乎都不支持整数的标记。

Goto

goto 指令(来自英文go和to的组合)是无条件流程控制指令中最基本的型式。一般在程式中会用以下的方式出现(指令大小写可能会依程式语言而不同)

gotolabel

goto 指令的效果是调整程式的控制流程,后续就运行标记位置的程式。

goto 指令是许多的计算机科学家视为有害(considered harmful)的指令,例如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:利用缩进(indentation)的层次,详细内容请参考Off-side规则

  • Haskell:可以利用缩进或大括弧,两者可以混用

有特定关键字的语言:Ada、Algol 68、 Modula-2(Modula-2)、Fortran 77、Visual Basic,使用的特定关键字依程式语言而不同:

  • Ada: 其关键字为 end + space + 启始控制结构的关键字,如if ... end if, loop ... end loop

  • Algol 68, 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

条件判断

条件判断是依指定变数或表达式的结果,决定后续运行的程式,最常用的是if-else指令,可以根据指定条件是否成立,决定后续的程式。也可以组合多个if-else指令,进行较複杂的条件判断。 许多程式语言也提供多选一的条件判断,例如C语言的switch-case指令。

循环

循环是指一段在程式中只出现一次,但可能会连续运行多次的代码。常见的循环可以分为二种,指定运行次数的循环(如C语言的for循环)以及指定继续运行条件(或停止条件)的循环(如C语言的while循环)。

在一些函式程式语言(例如Haskell和Scheme)中会使用递归或不动点组合子来达到循环的效果,其中尾部递归是一种特别的递归,很容易转换为叠代。

非区部控制流程

有些程式语言会提供非区部的控制流程(non-local control flow),会允许流程跳出目前的代码,进入一段事先指定的代码。常用的结构化非区部控制流程可分为条件处理(condition)、异常处理及延续性(Continuation)三种。

条件处理

PL/I程式语言中有22种标準的条件(如 ZERODIVIDE SUBSCRIPTRANGE ENDFILE),可以在程式中设定,当特定条件成立时需进行的指令,程式设计者也可以定义自己的条件,并在程式中使用。

条件成立时,只能设定一个需进行的指令(类似未结构化的if指令),大部份的套用中,都会指定运行goto指令,跳到其他代码运行对应的流程。

不过因为有些条件处理的实现会增加许多代码及运行时间(特别SUBSCRIPTRANGE),所以许多程式设计者会儘量不使用条件处理。

条件处理的语法如下:

ON 条件 GOTO label

异常处理

有些程式语言可提供不需要使用GOTO的结构化异常处理程式:

try{xxx1//Somewhereinherexxx2//use:'''throw'''someValue;xxx3}catch(someClass&someId){//catchvalueofsomeClassactionForSomeClass}catch(someType&anotherId){//catchvalueofsomeTypeactionForSomeType}catch(...){//catchanythingnotalreadycaughtactionForAnythingElse}

在try{...}的区块中,若 有异常情形时,程式就会离开try的区块,由后续的一个或多个catch子句判断需运行何种异常处理。在D、Java、C#及Python程式语言 中,try{...}区块中还可以加入一个finally子句,不管程式流程是否离开try{...}区块,finally子句中的程式都一定会运行,常 用在当程式退出处理时,需要放弃一些外部资源(档案或资料库连结)的情形下:

FileStreamstm=null;//C#exampletry{stm=newFileStream("logfile.txt",FileMode.Create);returnProcessStuff(stm);//maythrowanexception}finally{if(stm!=null)stm.Close();}

由于上述情形相当普遍,C#提供一种特殊的语法进行相同的处理:

using(FileStreamstm=newFileStream("logfile.txt",FileMode.Create)){returnProcessStuff(stm);//maythrowanexception}

只要离开 using区块,编译器会自动释放stm对象,Python的 with指令及也有类似的功能。

这些语言都有定义标準的异常情形及其出现的条件,程式设计者也可以丢出自己产生的异常(其实C++及Python的throw和catch支持绝大多数形态的对象)。

若某一个throw指令找不到对应的catch,控制流程会离开目前的副程式或控制结构,设法找到对应的catch,若到主程式的结尾还是找不到对应的catch,程式会强制退出,并显示适当的错误信息。

AppleScript脚本语言可以将"try"区块分为几个部份,提供不同的信息及异常:

trysetmyNumbertomyNumber/0onerrorenumbernfromftotpartialresultprif(e="Can'tdividebyzero")thendisplaydialog"Youidiot!"endtry

延续性

延续性(Continuation)可以将目前子程式的运行状态(包括目前的堆叠,区部变数及运行到的位置)存储成一个对象,后续在其他子程式中可以利用此对象回到此子程式现在的运行状态。

延续性一词也可以指第一类延续性(first-class continuation),是指程式语言可以在任意时间点存储目前的运行状态,并在之后回到之前存储的运行状态。

程式需要分配空间给子程式用到的区域变数,而且在子程式退出时需发布这些变数用到的空间。许多程式语言利用调用堆叠来存放这些变数,可以简单快速的分配及发布空间。也有一些程式语言使用动态存储器分配来存储变数,可以较灵活的分配变数,但分配及发布空间较不方便。这二个架构下延续性的处理方式也会不同,各有其优点及缺点。

Scheme语言利用call-with-current-continuation函式(缩写为call/cc) 可提供延续性功能。

相关词条

相关搜索

其它词条