1. 异常现象解析:当集合在枚举过程中被修改时会发生什么
这个异常是.NET开发中最常见的运行时错误之一,通常出现在使用foreach循环遍历集合时,代码同时尝试修改该集合内容。想象你正在图书馆按书架顺序清点图书,突然有人从你正在清点的书架上抽走一本书——这就是这个异常背后的场景。
典型错误场景示例:
List<string> books = new List<string>() { "C#入门", ".NET高级编程", "ASP.NET核心" }; foreach (var book in books) { if (book.Contains("入门")) { books.Remove(book); // 这里会抛出异常 } }2. 底层原理深度剖析
2.1 枚举器的实现机制
在.NET中,foreach循环实际上是通过IEnumerator实现的。当开始枚举时,集合会创建一个"快照"状态。任何对原始集合的修改都会使这个快照失效,就像在复印文件时有人修改了原件。
关键实现细节:
- GetEnumerator()调用时创建枚举器对象
- MoveNext()和Current属性基于集合初始状态工作
- 修改操作会触发集合版本号检查
2.2 版本号校验机制
大多数集合类内部维护一个_version字段。每次修改集合时这个值递增。枚举器在MoveNext()时会检查:
if (version != collection._version) { throw new InvalidOperationException("集合已修改..."); }3. 解决方案全景指南
3.1 标准解决方案:使用ToArray()或ToList()
最安全的做法是先创建副本:
foreach (var book in books.ToList()) { // 创建副本 if (book.Contains("入门")) { books.Remove(book); // 现在安全了 } }注意:大型集合要考虑内存开销,建议评估集合规模
3.2 高级方案:自定义线程安全集合
对于并发场景,可以实现线程安全包装器:
public class SafeCollection<T> : ICollection<T> { private readonly List<T> _items = new List<T>(); private readonly object _lock = new object(); public IEnumerator<T> GetEnumerator() { lock (_lock) { return new List<T>(_items).GetEnumerator(); } } // 其他成员实现... }3.3 替代方案:使用for循环倒序处理
当需要删除元素时,倒序遍历可以避免索引错位:
for (int i = books.Count - 1; i >= 0; i--) { if (books[i].Contains("入门")) { books.RemoveAt(i); } }4. 实战问题排查手册
4.1 典型错误模式识别表
| 错误模式 | 示例代码 | 修复方案 |
|---|---|---|
| 直接修改 | foreach中Remove | 使用ToList() |
| 多线程访问 | 并发枚举和修改 | 加锁或并发集合 |
| 延迟执行 | LINQ查询后修改源 | 立即执行(.ToList()) |
| 事件触发 | 枚举时事件修改集合 | 使用中间集合 |
4.2 性能优化建议
- 小型集合(<100项):ToArray()更优
- 中型集合(100-10,000项):ToList()更佳
- 大型集合(>10,000项):考虑for循环
- 高频操作:预分配容量或使用专用集合类
5. 高级应用场景
5.1 观察者模式中的陷阱
在事件处理中常见的问题:
// 错误示例 public event EventHandler DataChanged; foreach (var handler in DataChanged.GetInvocationList()) { DataChanged -= handler; // 会抛出异常 } // 正确做法 var handlers = DataChanged?.GetInvocationList(); if (handlers != null) { foreach (var handler in handlers.ToArray()) { DataChanged -= (EventHandler)handler; } }5.2 LINQ查询的特殊情况
延迟执行导致的隐蔽问题:
var query = books.Where(b => b.Contains("NET")); books.Add(".NET性能优化"); // 可能影响查询结果 foreach (var book in query) { // 这里可能抛出异常 // ... }解决方法:
var result = books.Where(b => b.Contains("NET")).ToList(); // 立即执行6. 最佳实践总结
经过多年项目实践,我总结出以下黄金法则:
- 只读遍历用foreach,需要修改就用for
- 多线程环境首选并发集合(ConcurrentBag等)
- LINQ查询要立即执行后再修改源集合
- 大型集合操作考虑分批处理
- 自定义集合类要实现完整的线程安全
一个专业级的解决方案模板:
public static void SafeRemove<T>(this IList<T> list, Func<T, bool> predicate) { var itemsToRemove = list.Where(predicate).ToList(); foreach (var item in itemsToRemove) { list.Remove(item); // 安全操作 } } // 使用:books.SafeRemove(b => b.Contains("入门"));这个异常虽然常见,但只要理解其背后的机制,就能写出更健壮的集合操作代码。我在实际项目中发现,90%的这类问题都可以通过预先创建副本来避免,剩下的10%则需要更精细的线程控制。