引言:认识一下这个让代码崩溃过无数次的错误
你是否曾经写过一个简单的 foreach 循环,运行程序时,突然——砰!——抛出了一个 InvalidOperationException,并附带一条吓人的消息:"Collection was modified; enumeration operation may not execute"?如果是这样,你并不孤单。这个问题在 Stack Overflow 上获得了数千次浏览,无论是新手还是经验丰富的开发者都会遇到。
在本文中,我们将分析这个错误产生的原因、如何复现它,以及最重要的是——如何避免它。我们将介绍三种经过验证的方法,编写真实的 C# 代码,并为您提供清晰的行动方案。开始吧!
什么是“Collection was modified”?错误解析
Collection was modified; enumeration operation may not execute 错误是一种 InvalidOperationException 类型的异常,当您尝试在通过 foreach 或其他枚举器(IEnumerator)遍历集合的同时修改集合(添加、删除或替换元素)时,就会抛出此异常。
想象一下,您正在读一本书,而有人同时从书中撕掉了几页。您会感到困惑,不知道接下来该读什么。枚举器也是如此:如果集合在遍历过程中发生变化,它不知道如何继续。
导致错误的代码长什么样?
下面是一个典型的例子,它肯定会引发错误:
List<string> fruits = new List<string> { "apple", "banana", "cherry", "date" };
foreach (var fruit in fruits){ if (fruit == "banana") { fruits.Remove(fruit); // 错误!集合已被修改 }}运行这段代码——您会在第二次迭代时得到 InvalidOperationException。为什么?因为 foreach 使用了一个内部枚举器,它在遍历期间会阻止对集合的修改。
为什么会发生这种情况?深入 C# 内部机制
当您编写 foreach 时,C# 编译器会将其转换为对 GetEnumerator() 方法的调用。该方法返回一个 IEnumerator 对象,该对象存储遍历状态(当前索引、集合版本)。每次集合发生变化时(通过 Add、Remove、Clear 等),其内部版本计数器都会增加。枚举器在每一步都会检查这个版本。如果版本不匹配——就会抛出异常。
这样做是为了防止未定义的行为。想象一下,您从列表中间删除一个元素:所有后续元素的索引都会移动,枚举器可能会跳过一个元素或重复处理同一个元素。.NET 开发者决定,抛出异常比允许一个不易察觉的逻辑错误更好。
避免“Collection was modified”错误的 3 种最佳方法
现在我们已经认清了敌人,让我们来分析如何战胜它。主要有三种方法,每种方法适用于不同的情况。
方法 1:在遍历前复制集合
最简单、最安全的方法——创建集合的副本并遍历它,同时对原始集合进行修改。
List<string> fruits = new List<string> { "apple", "banana", "cherry", "date" };
// 创建列表副本用于遍历foreach (var fruit in fruits.ToList()) // 或 new List<string>(fruits){ if (fruit == "banana") { fruits.Remove(fruit); // 现在这是安全的 }}
Console.WriteLine(string.Join(", ", fruits)); // 输出:apple, cherry, date优点: 简单、可读性强,适用于小型集合。
缺点: 需要额外内存,对于大型列表可能较慢。
提示: 对