foreach 按顺序为集合中的每个元素执行一次循环体,不需要管理索引:
输出:
Welcome, Ava
Welcome, Noah
Welcome, Mia
Total: 255
[C][#][7]
这个循环在数组、List<T>、字符串(元素类型是 char)、HashSet<T>、LINQ 查询或任何其他序列上的用法都一样。声明的类型可以写成 var,元素类型名很长时常这样写:foreach (var order in pendingOrders)。
foreach 在幕后做了什么
foreach 向集合请求一个枚举器,然后逐步遍历它。对于 List<int>,编译器大致会把循环转换成这样:
List<int>.Enumerator e = scores.GetEnumerator();
try
{
while (e.MoveNext())
{
int score = e.Current;
// loop body
}
}
finally
{
e.Dispose();
}
由此得出三个结论。任何拥有公共 GetEnumerator() 方法的类型都能用于 foreach,通常通过实现 IEnumerable<T> 来提供这个方法。即使循环体抛出异常或执行了 break,枚举器也会被释放。而且元素变量在每一轮都是 e.Current 的一个新副本,这就是给它赋值无法改变集合的原因。
没有 GetEnumerator 的类型不能使用 foreach:
error CS1579: foreach statement cannot operate on variables of type 'Order' because 'Order' does not contain a public instance or extension definition for 'GetEnumerator'
为该类型实现 IEnumerable<T>,或者遍历它包含的集合(用 order.Lines 而不是 order)。你还可以用 yield return 编写自己的序列,这在 IEnumerable 和 yield 那一页介绍。
遍历字典
字典的元素是 KeyValuePair<TKey, TValue> 值,带有 Key 和 Value 属性:
输出:
apples: 12
pears: 0
kiwis: 30
<apples><pears><kiwis>
Units in stock: 42
30 kiwis
12 apples
0 pears
Dictionary 不保证任何遍历顺序。实际上在你删除条目之前,它会按插入顺序排列,但依赖顺序的代码应该显式排序,就像最后一个循环用 OrderByDescending 那样,或者使用 SortedDictionary。
在 .NET Core 2.0 及以上版本(以及所有现代 .NET)中,KeyValuePair 有一个 Deconstruct 方法,所以可以直接在循环头部把键值对拆成两个变量:
// modern .NET: KeyValuePair deconstruction
foreach (var (fruit, count) in stock)
{
Console.WriteLine($"{fruit}: {count}");
}
这是最简短的写法,也是你在现在的代码中会看到的写法。它在 .NET Framework 上无法编译,因为那里的 KeyValuePair 没有 Deconstruct;在那里请使用 item.Key 和 item.Value。
获取索引
foreach 本身没有索引。获取索引通常有两种办法:自己维护一个计数器,或者使用 LINQ 的 Select 重载,它会把索引和每个元素一起传进来:
输出:
1. Kenji
2. Sofia
3. Omar
index 0: Kenji
index 1: Sofia
index 2: Omar
计数器开销最小,也最清晰。如果你发现自己要用索引读取 podium[i - 1] 或者写入 podium[i] = ...,那就换成 for 循环,它正是为此而生的。
循环变量是只读的
你不能给迭代变量赋值:
foreach (string name in names)
{
name = name.Trim();
// error CS1656: Cannot assign to 'name' because it is a 'foreach iteration variable'
}
即使允许赋值,也只会改变一个局部副本。要转换每个元素,请构建一个新序列(names.Select(n => n.Trim()).ToArray()),或者使用 for 循环写入 names[i] = names[i].Trim()。
你能做什么,取决于元素是类还是结构体。如果是类,变量保存的是引用,所以修改属性会改变集合中的那个对象:
输出:
Order 1: shipped
Order 2: shipped
如果 Order 是 struct,同样的赋值会因 CS1654 报错(Cannot modify members of 'order' because it is a 'foreach iteration variable'),因为变量是值的副本,而不是指向它的引用。
修改集合:InvalidOperationException
在 foreach 遍历 List 或 Dictionary 时添加或删除元素,会破坏枚举器。下一次 MoveNext 会抛出 InvalidOperationException,消息为“Collection was modified; enumeration operation may not execute.”(中文版 .NET 显示为“集合已修改;可能无法执行枚举操作。”):
输出:
InvalidOperationException: collection was modified
milk, bread
milk, bread
解决办法,从最简单到最灵活:
- 对
List<T>使用RemoveAll(predicate),一次遍历就删除所有匹配项。 - 用
ToList()或ToArray()遍历一个副本,然后就可以随意修改原集合。 - 先收集,后应用:在循环中把要删除的键收集到另一个列表里,循环结束后再删除。这是处理
Dictionary的常用模式。 - 使用 倒序的
for循环,配合RemoveAt(i)。
Dictionary 有一个值得了解的例外。从 .NET Core 3.0 开始,在 foreach 遍历字典时覆盖已有键的值(stock[key] = 0)或删除条目不再抛出异常,但添加新键仍然会抛出。在 .NET Framework 上这三种操作都会抛出异常,所以先收集键的做法在任何地方都能用。
break、continue 与 null 集合
break 和 continue 的行为和其他所有循环中一样:
输出:
reading 21
reading 23
no tags, no crash
遍历一个 null 集合时,调用 GetEnumerator 会抛出 NullReferenceException。?? Enumerable.Empty<T>() 把可能为 null 的序列变成一个空序列;更好的做法是,在你自己的方法中返回空集合而不是 null。
foreach 与 List.ForEach 的区别
List<T> 有一个 ForEach(Action<T>) 方法:names.ForEach(n => Console.WriteLine(n));。它和 foreach 循环并不相同。它的主体是一个 lambda,所以不能使用 break、continue 和 yield,而 return 只会结束当前元素的那次调用。它只存在于 List<T> 上,数组和其他序列都没有。优先使用 foreach 语句;只有当你已经有一个现成的方法、只需一行调用时,才用 ForEach。
常见问题
C# 中如何用 foreach 遍历 Dictionary?
每个元素都是一个 KeyValuePair<TKey, TValue>:foreach (var pair in prices) Console.WriteLine($"{pair.Key}: {pair.Value}");。在 .NET Core 2.0 及以上版本中可以对它解构:foreach (var (name, price) in prices)。如果只想遍历键或值,使用 prices.Keys 或 prices.Values。
C# 的 foreach 循环中如何获取索引?
foreach 没有内置的索引。可以自己维护一个计数器(循环前写 int i = 0;,循环体末尾写 i++),或者用 LINQ 把每个元素和它的索引一起投影出来:foreach (var item in names.Select((name, index) => new { name, index }))。如果需要用索引写回元素或比较相邻元素,for 循环更合适。
“Collection was modified; enumeration operation may not execute”是什么意思?
你在 foreach 遍历一个 List、Dictionary 或其他集合时,向它添加或从中删除了元素。枚举器发现了这个变化,在下一步抛出 InvalidOperationException。解决办法:遍历一个副本(foreach (var x in list.ToList())),先收集要做的修改、循环结束后再应用,使用 list.RemoveAll(...),或者使用倒序的 for 循环。
为什么在 C# 中不能给 foreach 变量赋值?
迭代变量是只读的:在 foreach (string name in names) 中写 name = name.Trim(); 会报错 CS1656。何况给它赋值本来也不会改变集合。要替换元素,请使用带索引的 for 循环,或者用 Select 构建一个新集合。修改类对象的属性(order.Status = ...)则是允许的。
C# 中可以跳出 foreach 循环吗?
可以。break 立即结束循环,continue 跳到下一个元素,和 for、while 中完全一样。return 会离开整个方法。List<T>.ForEach 方法则不同:它的循环体是一个 lambda,所以不能在里面使用 break 和 continue。