循环 (控制流程)
循环是电脑科学运算领域的用语,也是一种常见的控制流程。循环是一段在程序中只出现一次,但可能会连续执行多次的代码。循环中的代码会执行特定的次数,或者是执行到特定条件成立时结束循环,或者是针对某一集合中的所有项目都执行一次。
在一些函数编程语言(例如Haskell和Scheme)中会使用递归或不动点组合子来达到循环的效果,其中尾部递归是一种特别的递归,很容易转换为迭代。[1]
指定执行次数的循环(for loop)
大部分编程语言都提供循环的指令,可以依指定的次数重复执行一段程序。
若指定的次数N小于1,编程语言会忽略整个循环不去执行,若指定的次数N为1,则循环只会执行一次。
在循环进行时,循环计数器也会随着变化,大部分的编程语言可以允许循环计数器上数或是下数,每次的变化量可以是1或是其他不为0的数值。
FOR I = 1 TO N for I := 1 to N do begin xxx xxx NEXT I end; DO I = 1,N for ( I=1; I<=N; ++I ) { xxx xxx END DO }
在许多编程语言中,循环计数器要使用整数才能得到准确的结果。由于硬件的限制,在循环计数器使用浮点数时,结果可能会不符预期,如以下的循环
for X := 0.1 step 0.1 to 1.0 do
依其四舍五入的误差、硬件及编译器的差异,不一定会执行10次,可能只会执行9次。而且X的数值可能会有些误差,不是预期的0.1, 0.2, 0.3, ..., 1.0。
指定条件的循环(while loop/doWhile loop)
大多数的编程语言都有指令,可以在特定条件成立时继续循环的进行,或是特定条件不成立时继续循环的进行,进行到特定条件成立为止。前者一般会标示while,后者一般会标示until。
其判断条件可能在循环一开始就进行,或是在循环最后才进行。前者的循环不一定会执行,而后者1的循环至少会执行一次。
DO WHILE (test) repeat xxx xxx LOOP until test; while (test) { do xxx xxx } while (test);
指定集合的循环
许多编程语言支持一种特别的循环,可以针对一个数组中的元素或是一个集合中的所有成员进行循环中的指令,包括Ada、D语言、Smalltalk、Perl、C#、Visual Basic、Ruby、Python、Java、JavaScript、Fortran 95等编程语言都有这类的循环结构:
someCollection do: [:eachElement |xxx]. foreach (item; myCollection) { xxx } foreach someArray { xxx } Collection<String> coll; for (String s : coll) {} foreach (string s in myStringCollection) { xxx } $someCollection | ForEach-Object { $_ } forall ( index = first:last:step... )
泛用循环结构
有些编程语言有泛用循环结构,可以用来表示指定次数或指定条件的循环,像C语言的for指令或是Common Lisp语言中的do指令都是这类的例子,不过为了程序的可读性考量,在这些编程语言中还是尽量使用一些含义较明确的指令(如while指令)。
死循环
死循环一般会用在有一段程序需要永远执行,或是该程序在没有发生特殊事件(如故障)时需要永远执行的场合,例如一个事件驱动的程序需要持续执行循环,处理发生的事件,直到用户结束或中断程序为止。
若在指定条件的循环中,其判断条件用到的变量数值永远不会改变,这种程序错误也会使得此循环变成死循环。
提早结束整个循环
当使用指定次数的循环查表时,会希望在查到需要的资料时就可以直接结束循环的进行,有些编程语言可以用break
或exit
的指令达到这様的功能,这些指令会结束这个循环,接着会执行循环后面的指令。若此循环在副程序中,也可以用return
中断循环的进行, 同时离开副程序。
以下是Ada编程语言的一个示例,利用exit ... when...
的方式中提早结束循环。
with Ada.Text IO;
with Ada.Integer Text IO;
procedure Print_Squares is
X : Integer;
begin
Read_Data : loop
Ada.Integer Text IO.Get(X);
exit Read_Data when X = 0;
Ada.Text IO.Put (X * X);
Ada.Text IO.New_Line;
end loop Read_Data;
end Print_Squares;
Python支持一个特别的条件判断式,可以根据最近使用循环是否曾用break
提早结束而做不同的处理,举例如下:
for n in set_of_numbers:
if isprime(n):
print "Set contains a prime number"
break
else:
print "Set did not contain any prime numbers"
上例中的else
子句是for
循环的一部分,不是内层if
区块的一部分。Python语言的for
循环及while
循环都支持else
子句,当循环没有用break
提早结束时就会执行。
循环的特殊指令
有时在使用循环的程序中会希望在特定情形下跳过目前循环区块的指令,回到循环开始执行下一个循环,一般这类的指令会命名为continue
、skip
或next
,其效果是提早结束这次循环的进行,继续进行下一个循环,若此循环已经是最后一次执行,这类指令会结束循环的进行,继续进行后续的指令。
像Perl及Ruby等编程语言有redo
指令,可以重新执行目前的循环,若在指定次数的循环中,其循环计数器的数值不会变化。Ruby编程语言有retry
指令,可以让循环计数器回到初值,重新执行整个循环。
循环变量及循环不变量
循环变量是一个初值不为负的整数表示式,在每次执行循环时循环变量的数值需减少,但在正常的循环执行过程中循环变量的数值不会变成负值。循环变量用来确保循环会结束。
循环不变量是一个和循环有关的判断式,在第一次进入循环之前,循环不变量的值需为真,在后续每一次执行循环时,其值也要为真。当循环正确的结束时,其终止条件和循环不变量都会成立。循环不变量可用来监控在循环进行时,某一指定性质的状态。
像是Eiffel之类的编程语言本身就有支持循环变量及循环不变量,其他语言可能需要有附加组件才能支持此功能,例如Java就需要配合Java建模语言规范的loop statements (页面存档备份,存于互联网档案馆)才能支持此机能。
不同语言的循环比较表
编程语言 | 条件判断式 | 循环 | early exit | continuation | redo | retry | 正确性判断机制 | ||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
循环启始 | 循环中间 | 循环结尾 | 指定次数 | 指定集合 | 泛用 | 无穷[1] | 循环变量 | 循环不变量 | |||||
Ada | 是 | 是 | 是 | 是 | 只针对数组 | 否 | 是 | 多层循环 | 否 | ||||
C | 是 | 否 | 是 | 否 [2] | 否 | 是 | 否 | 多层循环 [3] | 多层循环 [3] | 否 | |||
C++ | 是 | 否 | 是 | 否 [2] | 是 | 是 | 否 | 多层循环 [3] | 多层循环 [3] | 否 | |||
C# | 是 | 否 | 是 | 否 [2] | 是 | 是 | 否 | 多层循环 [3] | 多层循环 [3] | ||||
Common Lisp | 是 | 是 | 是 | 是 | 是 | 是 | 是 | 多层循环 | 否 | ||||
Eiffel | 是 | 否 | 否 | 是 [10] | 是 | 是 | 否 | 一层循环 [10] | 否 | 否 | 否 [11] | 是 | 是 |
F# | 是 | 否 | 否 | 是 | 是 | 否 | 否 | 否 [6] | 否 | 否 | |||
FORTRAN 77 | 是 | 否 | 否 | 是 | 否 | 否 | 否 | 一层循环 | 是 | ||||
FORTRAN 90 | 是 | 否 | 否 | 是 | 否 | 否 | 是 | 多层循环 | 是 | ||||
FORTRAN 95及后续版本 | 是 | 否 | 否 | 是 | 数组 | 否 | 是 | 多层循环 | 是 | ||||
Haskell | 否 | 否 | 否 | 否 | 是 | 否 | 是 | 否 [6] | 否 | 否 | |||
Java | 是 | 否 | 是 | 否 [2] | 是 | 是 | 否 | 多层循环 | 多层循环 | 否 | 非原生(non-native) [12] | 非原生(non-native) [12] | |
JavaScript | 是 | 否 | 是 | 否 [2] | 是 | 是 | 否 | 多层循环 | 多层循环 | 否 | |||
OCaml | 是 | 否 | 否 | 是 | 数组及列表(list) | 否 | 否 | 否 [6] | 否 | 否 | |||
PHP | 是 | 否 | 是 | 否 [2] [5] | 是 [4] | 是 | 否 | 多层循环 | 多层循环 | 否 | |||
Perl | 是 | 否 | 是 | 否 [2] [5] | 是 | 是 | 否 | 多层循环 | 多层循环 | 是 | |||
Python | 是 | 否 | 否 | 否 [5] | 是 | 否 | 否 | 多层循环 [6] | 多层循环 [6] | 否 | |||
REBOL | 否 [7] | 是 | 是 | 是 | 是 | 否 [8] | 是 | 一层循环 [6] | 否 | 否 | |||
Ruby | 是 | 否 | 是 | 是 | 是 | 否 | 是 | 多层循环 [6] | 多层循环 [6] | 是 | 是 | ||
Standard ML | 是 | 否 | 否 | 否 | 数组,列表(list) | 否 | 否 | 否 [6] | 否 | 否 | |||
Visual Basic .NET | 是 | 否 | 是 | 是 | 是 | 否 | 是 | 每种循环一层 | 每种循环一层 | ||||
Windows PowerShell | 是 | 否 | 是 | 否 [2] | 是 | 是 | 否 | ? | 是 |
- a 此项只考虑专用的死循环程序结构,因此
while (true)
和for ( ; ; )
都不算在内。 - a b c d e f g h C语言的
for (init; test; increment)
循环是一个通用的循环指令,累加量也不一定要为1。 - a b c 在C、C++及C#中,跳出多层循环可以用label和goto指令达到。
- a 在PHP 5中已支持 (页面存档备份,存于互联网档案馆)配合物件的循环。
- a b c 指定次数的循环可以用重复incrementing list或generator的方式来达到其效果,例如Python中的
range()
。 - a b c d e 可以用异常处理来跳出多层循环。
- a 没有专门指令,但
while
指令可以用作此用途。 - a 没有专门指令,但用户可以定义通用循环指令。
- a 正在计划的C++0x标准中已加入以范围为基础的 for 循环。标准模板库(STL)中有
std::for_each
模板函数,可以对STL容器(container)的每个元素重复调用一个一元函数[3]。制作STL容器的宏也可以达到类似的效果[4]。 - a 利用整数区间的迭代可以达到指定次数循环的效果, early exit可以用多增加一个exit的条件来达成。
- a Eiffel支持保留字
retry
,不过是用在契约式设计的异常处理,不是循环的流程控制指令。 - a 需要配合Java建模语言(JML)。
参考资料
- ^ arun_yh. 循环(loop), 递归(recursion), 遍历(traversal), 迭代(iterate)的区别 - arun_yh - 博客园. www.cnblogs.com. 部落格园. [2017-03-09]. (原始内容存档于2017-03-12) (中文(中国大陆)).
- ^ Meyer, Bertrand. Eiffel: The Language. Prentice Hall. 1991: 129~131.
- ^ for_each (页面存档备份,存于互联网档案馆). Sgi.com. Retrieved on 2010-11-09.
- ^ Chapter 1. Boost.Foreach (页面存档备份,存于互联网档案馆). Boost-sandbox.sourceforge.net (2009-12-19). Retrieved on 2010-11-09.