循环 (控制流程)

回圈计算机科学运算领域的用语,也是一种常见的控制流程。回圈是一段在程式中只出现一次,但可能会连续执行多次的程式码。回圈中的程式码会执行特定的次数,或者是执行到特定条件成立时结束回圈,或者是针对某一集合中的所有项目都执行一次。

在一些函数程式语言(例如HaskellScheme)中会使用递归不动点组合子来达到回圈的效果,其中尾部递归是一种特别的递归,很容易转换为迭代[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);

指定集合的回圈

许多程式语言支援一种特别的回圈,可以针对一个阵列中的元素或是一个集合中的所有成员进行回圈中的指令,包括AdaD语言SmalltalkPerlC#Visual BasicRubyPythonJavaJavaScriptFortran 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指令)。

无穷回圈

无穷回圈一般会用在有一段程式需要永远执行,或是该程式在没有发生特殊事件(如故障)时需要永远执行的场合,例如一个事件驱动的程式需要持续执行回圈,处理发生的事件,直到使用者结束或中断程式为止。

若在指定条件的回圈中,其判断条件用到的变数数值永远不会改变,这种程式错误也会使得此回圈变成无穷回圈。

提早结束整个回圈

当使用指定次数的回圈查表时,会希望在查到需要的资料时就可以直接结束回圈的进行,有些程式语言可以用breakexit的指令达到这様的功能,这些指令会结束这个回圈,接著会执行回圈后面的指令。若此回圈在副程式中,也可以用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提早结束时就会执行。

回圈的特殊指令

有时在使用回圈的程式中会希望在特定情形下跳过目前回圈区块的指令,回到回圈开始执行下一个回圈,一般这类的指令会命名为continueskipnext,其效果是提早结束这次回圈的进行,继续进行下一个回圈,若此回圈已经是最后一次执行,这类指令会结束回圈的进行,继续进行后续的指令。

像Perl及Ruby等程式语言有redo指令,可以重新执行目前的回圈,若在指定次数的回圈中,其回圈计数器的数值不会变化。Ruby程式语言有retry指令,可以让回圈计数器回到初值,重新执行整个回圈。

回圈变式及回圈不变式

回圈变式英语Loop variant回圈不变式是判断回圈正确性的工具[2]

回圈变式是一个初值不为负的整数表示式,在每次执行回圈时回圈变式的数值需减少,但在正常的回圈执行过程中回圈变式的数值不会变成负值。回圈变式用来确保回圈会结束。

回圈不变式是一个和回圈有关的判断式,在第一次进入回圈之前,回圈不变式的值需为真,在后续每一次执行回圈时,其值也要为真。当回圈正确的结束时,其终止条件和回圈不变式都会成立。回圈不变式可用来监控在回圈进行时,某一指定性质的状态。

像是Eiffel之类的程式语言本身就有支援回圈变式及回圈不变式,其他语言可能需要有附加元件才能支持此功能,例如Java就需要配合Java建模语言英语Java Modeling Language规范的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] ?
  1. a 此项只考虑专用的无穷回圈程式结构,因此while (true)for ( ; ; )都不算在内。
  2. a b c d e f g h C语言的for (init; test; increment)回圈是一个通用的回圈指令,累加量也不一定要为1。
  3. a b c 在C、C++及C#中,跳出多层回圈可以用label和goto指令达到。
  4. a 在PHP 5中已支援页面存档备份,存于互联网档案馆)配合物件的回圈。
  5. a b c 指定次数的回圈可以用重复incrementing list或generator的方式来达到其效果,例如Python中的range()
  6. a b c d e 可以用异常处理来跳出多层回圈。
  7. a 没有专门指令,但while指令可以用作此用途。
  8. a 没有专门指令,但使用者可以定义通用回圈指令。
  9. a 正在计划的C++0x标准中已加入以范围为基础的 for 回圈标准模板库(STL)中有std::for_each模板函数,可以对STL容器(container)的每个元素重复呼叫一个一元函数[3]。制作STL容器的巨集也可以达到类似的效果[4]
  10. a 利用整数区间的迭代可以达到指定次数回圈的效果, early exit可以用多增加一个exit的条件来达成。
  11. a Eiffel支援保留字retry,不过是用在契约式设计异常处理,不是回圈的流程控制指令。
  12. a 需要配合Java建模语言英语Java Modeling Language(JML)。

参考资料

  1. ^ arun_yh. 循环(loop), 递归(recursion), 遍历(traversal), 迭代(iterate)的区别 - arun_yh - 博客园. www.cnblogs.com. 博客园. [2017-03-09]. (原始内容存档于2017-03-12) (中文(中国大陆)). 
  2. ^ Meyer, Bertrand. Eiffel: The Language. Prentice Hall. 1991: 129~131. 
  3. ^ for_each页面存档备份,存于互联网档案馆). Sgi.com. Retrieved on 2010-11-09.
  4. ^ Chapter 1. Boost.Foreach页面存档备份,存于互联网档案馆). Boost-sandbox.sourceforge.net (2009-12-19). Retrieved on 2010-11-09.