一、foreach 本质编译成了什么1. 源码写法Listint list new Listint { 1,2,3,4 }; foreach (var item in list) { Console.WriteLine(item); }2. 编译器语法糖解糖后真实代码Listint list new Listint { 1,2,3,4 }; // 获取迭代器 Listint.Enumerator enumerator list.GetEnumerator(); try { // 循环向后移动指针 while (enumerator.MoveNext()) { int item enumerator.Current; Console.WriteLine(item); } } finally { // 释放迭代器资源 enumerator.Dispose(); }核心三个角色IEnumerable提供GetEnumerator()IEnumerator提供MoveNext()、Current、Reset()Enumerator是 List 内部结构体迭代器二、List 内部版本号机制1. ListT 里有个隐藏字段// ListT 源码内部 private int _version;只要对集合做结构性修改_version哪些操作会让版本号自增Add、AddRangeRemove、RemoveAt、RemoveRangeInsert、Clear、Reverse、Sort改变结构 / 顺序只读操作索引访问、遍历读不改变版本号2. 迭代器初始化时会快照版本号List 的迭代器结构体内部也有一个字段internal struct Enumerator : IEnumeratorT { private ListT _list; private int _index; // 记录遍历开始那一刻的集合版本 private int _version; private T _current; }遍历一开始迭代器把当前list._version存到自己_version里做快照。3. MoveNext () 第一步就校验版本每次调用MoveNext()第一件事if (_list._version ! _version) { throw new InvalidOperationException(集合已修改可能无法执行枚举操作。); }只要遍历中途你增删了元素list._version变了和迭代器保存的快照版本对不上直接抛异常。三、为什么 C# 要设计成「遍历中禁止增删」不是故意限制是为了规避三类致命问题漏遍历正向遍历删除元素后面元素前移下一次索引跳过一个元素重复遍历中间插入元素指针回头重复遍历索引越界、内存错乱动态扩容、缩容导致内部数组地址变化迭代器指针失效foreach 依赖迭代器顺序往后走没有索引概念一旦底层数组变了迭代器无法自洽所以微软直接用版本号校验一刀切保护。不是 foreach 不让改是 List、ArrayList、Dictionary 这类普通集合的迭代器自带版本校验。四、哪些修改会报错、哪些不会1. 会报错改变集合结构foreach 里执行Add / Remove / RemoveAt / Insert / Clear→ 版本号变化 → 抛异常2. 不会报错只改元素内容不改结构foreach (var item in list) { // 修改元素本身的值不增删、不改变集合个数 item 999; }只改元素内容不改动集合结构版本号不变不会报错。五、到底哪些方式可以遍历中增删方式一for 倒序遍历最常用、零额外开销原理从最后一个索引往前遍历删除元素时前面元素前移不影响还没遍历到的前面索引不会跳项、不会漏项。Listint list new Listint { 1,2,3,4,5 }; // 倒序 for (int i list.Count - 1; i 0; i--) { if (list[i] % 2 0) { list.RemoveAt(i); } }特点可以安全删除也可以插入、Add但插入建议别在循环里搞容易逻辑乱无版本校验报错性能最高无额外内存分配正向 for 为什么不行// 错误写法 for (int i 0; i list.Count; i) { if (条件) list.RemoveAt(i); // 删除后后面元素前移i 会跳过下一个元素 }方式二先遍历标记再统一删除业务最稳、可读性强先 foreach 遍历把要删除的元素 / 索引暂存到临时集合遍历结束后再循环删除Listint list new Listint { 1,2,3,4,5 }; // 1. 收集待删除项 var needDel list.Where(x x % 2 0).ToList(); // 2. 事后删除 foreach (var item in needDel) { list.Remove(item); }特点完全避开 foreach 中增删逻辑清晰适合复杂业务判断会多一个临时 List少量内存开销方式三List.RemoveAll1. 用法// 删除所有满足条件的元素 list.RemoveAll(x x % 2 0);2. 底层原理RemoveAll内部自己用 while 循环 数组移位不走 foreach 迭代器不触发版本号异常。内部大致逻辑遍历内部底层数组_items用一个写入索引把不满足删除条件的元素往前覆盖最后截断列表长度一次性清理只在最后修改一次版本号3. 优势一行代码搞定批量删除底层数组原地移位性能比自己循环删除更高不用关心索引、不用倒序不会报集合已修改异常4. 适用场景批量按条件删除是 List 最优方案之一。方式四复制原集合再遍历原集合增删// 复制一份快照 var temp new Listint(list); // 遍历快照操作原集合 foreach (var item in temp) { if (item 3) { list.Remove(item); } }原理遍历的是新复制的临时集合修改的是原集合两者互不干扰版本号校验互不影响。适合简单场景缺点是要完整复制集合大数据量有内存开销。方式五线程安全集合 可边遍历边增删普通集合不行并发集合没有版本号强校验ConcurrentBagTConcurrentQueueTConcurrentStackTConcurrentDictionaryTKey,TValueConcurrentDictionaryint, string dict new ConcurrentDictionaryint, string(); foreach (var kv in dict) { // 遍历中可以安全增删改不会抛集合已修改 dict.TryAdd(99, test); }特点天生支持遍历中增删内部加锁线程安全性能比普通 List 稍低适合多线程场景、上位机多设备并发队列场景方式六用 LinkedList 链表LinkedListT迭代器设计不同遍历中可以删除当前节点LinkedListint link new LinkedListint(); var node link.First; while (node ! null) { var next node.Next; // 先保存下一个节点 if (node.Value % 2 0) { link.Remove(node); } node next; }链表删除节点不影响其他节点索引不会错乱。六、总结foreach 不能增删根源foreach 编译成迭代器迭代器初始化快照集合版本号每次 MoveNext 校验版本增删会让_version版本不一致直接抛异常。禁止增删的目的防止遍历漏项、跳项、索引错乱、底层数组扩容导致指针失效。可以遍历中增删的 6 种方案for 倒序遍历删元素首选性能高先收集再批量删业务逻辑最清晰List.RemoveAll一行批量删底层优化性能最好复制集合快照遍历简单粗暴Concurrent 并发集合多线程随便边遍历边增删LinkedList 链表适合频繁增删节点场景只改元素内容、不改集合个数foreach 里是允许的不会报错。