foreach遍历List时不可修改集合,因IEnumerator依赖version字段检测变更;需删除时应使用反向for循环;Where+ToList适合筛选保留项但有内存开销;ReadOnlyCollection非真正只读。

用 foreach 遍历 List 是最安全、最常用的方式;但若需在遍历时修改集合(如删除元素),直接用 foreach 会抛出 InvalidOperationException: Collection was modified —— 这是绝大多数人踩的第一个坑。
为什么不能在 foreach 中删改 List
foreach 底层依赖 IEnumerator,它在迭代开始时会记录集合的 version 字段。只要调用 Add、Remove、Clear 等任何改变结构的操作,version 就会自增,下次 MoveNext() 就检测到不一致并立即报错。
常见错误现象:
- 代码看似逻辑正确(比如“找到匹配项就
list.Remove(item)”),运行时报Collection was modified - 错误堆栈指向
foreach的隐式MoveNext()调用,而非你写的Remove行 —— 容易误判问题位置
需要边遍历边删除时,该用什么
改用反向 for 循环,从 Count - 1 递减到 0:
for (int i = list.Count - 1; i >= 0; i--)
{
if (list[i].ShouldBeRemoved)
list.RemoveAt(i);
}
理由很实在:
- 索引访问
list[i]不触发版本检查 - 删除末尾或中间元素后,后续待检查的元素只会向前移,而你的
i在递减,不会跳过下一个有效索引 - 比
ToList()后再foreach更省内存,尤其对大列表
别用正向 for(i++)配 RemoveAt(i):删掉第 i 个后,原 i+1 位置的元素会补上来,但循环已自增 i,导致它被跳过。
Where + ToList 适合哪些场景
当你要“筛选出要保留的元素”而非“逐个判断是否删除”,用 LINQ 更清晰:
list = list.Where(x => !x.ShouldBeRemoved).ToList();
但要注意代价:
- 生成新列表,原引用丢失 —— 如果其他地方还持有原
list引用,它们不会自动更新 - 内存翻倍(旧列表暂未 GC,新列表已分配),对超大
List或含大对象的列表要谨慎 - 性能上,
O(n)时间但有额外 GC 压力;而反向for是原地操作,O(n)且无分配
如果只是临时查数据、不改原集合,foreach 仍是首选;如果逻辑复杂、涉及多条件组合筛选,Where 可读性明显更高。
别忽略 ReadOnlyCollection 和 IEnumerable 的陷阱
函数参数接收 IEnumerable 时,传入的可能是 List,也可能是 yield return 的惰性序列 —— 此时反复遍历可能触发多次实际计算(比如查数据库)。若需多次遍历,应先调用 .ToList() 缓存结果。
而返回只读包装时:
-
list.AsReadOnly()返回ReadOnlyCollection,它仍能通过反射或强转绕过保护,不是真正安全的只读 - 若真要防止外部修改,应返回
new List的副本,或用(list) ImmutableList(需引用System.Collections.Immutable)
真正难处理的,往往是那些你以为“只是读”的方法,内部悄悄调了 Remove 或 Clear,又没文档说明 —— 遇到诡异的 Collection was modified,优先查调用链里有没有隐藏的集合变更操作。











