Menu

C# for 循环:语法、倒序、嵌套循环与常见陷阱

C# 的 for 循环如何工作:初始化、条件和迭代三部分,倒序计数与按步长递增,多个循环变量,按索引遍历数组,嵌套循环,for(;;),以及差一错误和边遍历边删除的 bug。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

for 循环在条件为真时重复执行一段代码,初始设置和每一步的变化都写在循环头部。需要计数时就用它:固定次数的重复,或者数组的每一个索引。

输出:

Lap 1 of 5
Lap 2 of 5
Lap 3 of 5
Lap 4 of 5
Lap 5 of 5

三个部分的执行顺序

for (initializer; condition; iterator) 按固定顺序执行:

  1. 初始化部分(int i = 1)最先执行,而且只执行一次。
  2. 检查条件(i <= 5)。如果为 false,循环结束,循环体不再执行。
  3. 执行循环体。
  4. 执行迭代部分(i++),然后回到第 2 步。

因为条件在每次迭代之前检查,for 循环可能一次都不执行:for (int i = 10; i < 5; i++) 永远不会进入循环体。上面的循环相当于下面这个 while 循环:

int i = 1;
while (i <= 5)
{
    Console.WriteLine($"Lap {i} of 5");
    i++;
}

两种写法有一处不同:在 for 循环体中使用 continue 仍会执行迭代部分,而在这个 while 版本中它会跳过 i++,于是循环会再次检查同一个 i,可能永远转下去。在初始化部分声明的变量只存在于循环内部。如果之后还需要它的最终值,就在循环之前声明:int i; for (i = 0; ...; i++) { }。

倒序计数和按步长递增

迭代部分可以是任何表达式。递减可以倒数,加上大于一的数可以按步长前进,乘法则得到等比数列:

输出:

3... 2... 1... liftoff
 00 15 30 45
 1 10 100 1000

要确保迭代部分朝着让条件变为 false 的方向前进。for (int i = 0; i < 10; i--) 离 10 越来越远,只有在约二十亿次迭代后 i 溢出回绕时才会停止。

按索引遍历数组

数组有 Length,列表有 Count,有效索引是从 0 到这个值减一。索引能带给你 foreach 做不到的东西:知道当前位置,访问相邻元素,以及写回元素。

输出:

#1: 17.99
#2: 4.50
#3: 38.25
#4: 7.43
item 1 vs item 0: down
item 2 vs item 1: up
item 3 vs item 2: down

最后一个循环从 1 开始,因为它要把每个元素和前一个元素比较。数组本身的创建、排序和复制在数组那一页介绍。

多个循环变量

初始化部分可以声明多个同类型的变量,迭代部分也可以更新多个变量,用逗号分隔。经典用法是两个索引相向而行:

输出:

racecar is a palindrome: True
9, 1, 8, 3

在同一个声明中,所有变量必须是同一类型:int left = 0, right = 10 可以,int i = 0, string s = "" 不行。

嵌套循环

循环中的循环会在外层循环每迭代一次时完整地跑一遍。表格和网格是典型的场景:

输出:

   1   2   3   4
   2   4   6   8
   3   6   9  12
   4   8  12  16

内层循环体执行了 16 次(4 × 4)。嵌套会让工作量迅速成倍增长:两个各遍历 10,000 个元素的嵌套循环就是 1 亿次迭代。内层循环中的 break 只会离开内层循环;如何同时跳出两层,请看 break 和 continue 那一页。

死循环:for(;;)

头部的三个部分都是可选的。没有条件时,循环会一直运行,直到循环体内部的某个语句让它离开:

输出:

Month 1: 70 left
Month 2: 40 left
Month 3: 10 left
Overdrawn in month 4: -20

for (;;) 和 while (true) 编译后完全相同,选你的代码库习惯用的那个即可。

差一错误

最常见的 for bug 是条件多走了一步:

输出:

Ada
Grace
Linus
IndexOutOfRangeException at index 3

长度为 3 的数组索引是 0、1 和 2,所以正确的条件是 i < names.Length。与之对应的 bug 是用 i > 0 倒数,这样永远访问不到索引 0。在 List<T> 上犯同样的错误,抛出的则是 ArgumentOutOfRangeException。

遍历时删除元素

在正向循环中从 List<T> 删除元素,会让后面的每个元素都向前移动一位,于是每次删除后紧跟着的那个元素就被跳过了:

输出:

forward:  5, 0, 3, 7
backward: 5, 3, 7

正向循环漏掉了一个 0:删除索引 1 之后,第二个零移到了索引 1,但 i++ 已经前进到了索引 2。倒序循环可以避免这个问题,因为删除只会移动你已经访问过的元素。如果想用一行代码解决,stock.RemoveAll(x => x == 0) 也能做到同样的事。foreach 循环则根本不允许删除,它会抛出 InvalidOperationException。

浮点数计数器

避免用 double 做循环计数器并判断相等。0.1 没有精确的二进制表示,所以把它加十次并不会正好得到 1.0:

for (double x = 0; x != 1.0; x += 0.1)   // never ends: x skips past 1.0
{
}

应该用整数计数,再由它计算出所需的值:for (int i = 0; i <= 10; i++) { double x = i / 10.0; }。

for 与 foreach 的对比

需求使用
按顺序读取每个元素foreach
需要索引或相邻元素(i - 1、i + 1)for
给数组元素赋值for
按步长 2 前进、倒序遍历for
遍历时从 List 中删除元素for,倒序
没有索引器的集合(HashSet、Dictionary、LINQ 结果)foreach
重复某件事 N 次,不涉及集合for

在数组上,两种循环编译出的代码基本相同,所以按可读性选择,而不是按速度。

常见问题

C# 中 for 循环的语法是什么?

for (initializer; condition; iterator) { body },例如 for (int i = 0; i < 5; i++) { ... }。初始化部分只执行一次,条件在每次迭代之前检查,迭代部分在每次迭代之后执行。条件第一次为 false 时,循环结束。

C# 中如何倒序循环?

从最后一个索引开始递减:for (int i = items.Length - 1; i >= 0; i--)。注意是 >= 0 而不是 > 0,否则会漏掉第一个元素。倒序循环也是在遍历时从 List 中删除元素的安全做法。

C# 中什么时候用 for 而不是 foreach?

需要索引时(比较相邻元素、写回数组、步长大于一)、需要计数而不是遍历集合时,或者要从列表中删除元素时,用 for。只是按顺序读取每个元素时用 foreach:它更简短,也没有可能写错的索引。

C# 中如何写死循环?

for (;;) { ... } 或 while (true) { ... }。for 头部的三个部分都是可选的,省略条件就等于条件为 true。满足退出条件时,用 break 或 return 离开循环。

为什么我的 for 循环抛出 IndexOutOfRangeException?

条件让索引到达了长度:i <= array.Length 会访问 array[array.Length],也就是最后一个元素之后的位置。有效索引是从 0 到 Length - 1,所以条件应该写成 i < array.Length(对 List 则是 i < list.Count,它抛出的是 ArgumentOutOfRangeException)。

Coddy 编程语言插图

和 Coddy 一起学编程

开始