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) 按固定顺序执行:
- 初始化部分(
int i = 1)最先执行,而且只执行一次。 - 检查条件(
i <= 5)。如果为 false,循环结束,循环体不再执行。 - 执行循环体。
- 执行迭代部分(
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)。