在C#开发中,foreach是最常用的遍历语法之一。它不仅能遍历数组和List,还能操作实现了IEnumerable接口的任何类型。理解其底层机制和书写规范,是写出健壮代码的基础。下面通过具体场景和代码示例,系统讲解foreach的使用方式与注意事项。

foreach的基础语法与遍历List示例
foreach的语法结构非常直观:foreach (var item in collection)。其中collection必须是实现了IEnumerable或IEnumerable<T>接口的对象。编译器在背后会将其转换为对枚举器的调用,先获取GetEnumerator(),然后在循环中不断执行MoveNext()并读取Current属性,直到返回false。
下面是一个遍历List<string>的基础例子,展示如何安全地读取每个元素并打印:
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<string> names = new List<string> { "Alice", "Bob", "Charlie" };
foreach (string name in names)
{
Console.WriteLine("姓名: " + name);
}
}
}
上述代码在编译后等价于使用while循环调用枚举器,但foreach版本可读性更高,且避免了手动管理索引越界的问题。对于值类型元素,foreach中的迭代变量是只读的副本;对于引用类型,迭代变量持有对象的引用,可以修改对象内部状态,但不能将迭代变量重新指向新对象。
遍历字典与自定义迭代器的实现
除了线性集合,foreach也常用于遍历Dictionary。字典的每一项为KeyValuePair<TKey, TValue>,通过var关键字可以让编译器自动推断类型,减少冗余声明。在遍历过程中如果需要同时拿到键和值,直接解构即可。
我们还可以让自定义类支持foreach,只需实现IEnumerable<T>接口并提供GetEnumerator方法。以下示例定义一个简单的整数范围类,并返回范围内的每个数:
using System;
using System.Collections;
using System.Collections.Generic;
class IntRange : IEnumerable<int>
{
private int start;
private int end;
public IntRange(int s, int e)
{
start = s;
end = e;
}
public IEnumerator<int> GetEnumerator()
{
for (int i = start; i <= end; i++)
{
yield return i;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
class Test
{
static void Main()
{
IntRange range = new IntRange(1, 3);
foreach (int num in range)
{
Console.WriteLine(num);
}
}
}
这段代码使用了yield return语法,编译器会自动生成一个状态机枚举器,实现惰性迭代。也就是说,每次循环才计算下一个值,而不是一次性把所有数据装入内存。对于大型数据集合或流式处理,这种方式能显著降低内存占用。
字典遍历时还有一点需要注意:如果在foreach中尝试对字典本身进行增删操作,同样会抛出运行时异常。若必须修改,应先将键或值复制到临时列表中,再在循环外更新原字典。自定义迭代器如果依赖内部状态,也应在设计阶段就明确是否允许并发修改。
foreach中的常见错误与性能优化
最常见的错误是在foreach循环体内直接对集合调用Add或Remove。由于枚举器会跟踪集合版本号,任何结构性修改都会使版本号变化,从而触发InvalidOperationException。例如对List进行移除操作应使用倒序for循环,或借助RemoveAll方法。
从性能角度看,foreach对数组的遍历会被JIT优化为和for循环相近的机器码,几乎无额外开销;但对ArrayList等非泛型集合,由于涉及装箱拆箱,性能不如泛型List配合foreach。此外,如果迭代逻辑非常简单且需要索引,for循环可能更直观。下面展示一个错误示例及正确做法:
using System;
using System.Collections.Generic;
class Demo
{
static void Main()
{
List<int> nums = new List<int> { 1, 2, 3, 4 };
// 错误:在foreach中删除
// foreach (int n in nums)
// {
// if (n % 2 == 0) nums.Remove(n);
// }
// 正确:使用RemoveAll
nums.RemoveAll(n => n % 2 == 0);
foreach (int n in nums)
{
Console.WriteLine(n);
}
}
}
合理利用foreach不仅能减少bug,还能借助LINQ实现声明式数据处理。例如foreach (var x in list.Where(i => i > 0))可以清晰表达过滤意图。但需记住,LINQ查询默认也是延迟执行,只有在foreach迭代时才真正计算,因此要避免在同一集合上多次遍历导致重复计算,必要时用ToList缓存结果。
最后,在异步场景中不要在foreach里直接调用不含等待的异步方法而忽略Task,否则可能丢失异常或造成并发问题。若需并行处理,可考虑Parallel.ForEach,但要确保操作是线程安全的。掌握这些细节,才能把C#中foreach实例代码写得既规范又高效。