C# 遍历迭代完全指南for、foreach、while 怎么选foreach 底层原理、集合遍历与增删坑一篇讲透遍历也叫迭代说白了就是把一堆数据挨个访问一遍。它是编程里最高频的操作之一数组、List、字典、字符串……几乎天天都在写。可你是否清楚for和foreach到底有什么区别、该怎么选foreach背后悄悄做了什么为什么遍历集合时不能直接增删元素本文系统梳理 C# 中各种遍历方式、可遍历类型、foreach的底层原理IEnumerable/IEnumerator、yield以及高频踩坑点适合初学者建立完整知识体系也适合有基础的同学复习回顾。一、什么是遍历遍历就是按照一定规则依次访问一组数据中的每一个元素。一次遍历通常包含三个要素从哪开始起点如第 0 个元素怎么走每次前进的规则如索引 1何时结束终止条件如到达集合末尾。C# 中常用的遍历方式有for、foreach、while、do-while以及集合提供的ForEach方法。下面这张总览图先建立整体认识简单记忆for/while基于索引或条件由你手动控制循环foreach基于枚举器IEnumerable专门用来从头到尾遍历集合最简洁。二、遍历数组数组是最基础的数据结构for和foreach都能遍历。int[]nums{10,20,30,40,50};// 方式一for通过索引访问需要自己管理索引 i// i 从 0 开始每次 1直到等于数组长度 Length 时结束for(inti0;inums.Length;i){Console.WriteLine($下标{i}值{nums[i]});}// 方式二foreach不需要关心索引和长度直接拿到每个元素foreach(intninnums){Console.WriteLine(n);}for的优势是可以灵活控制步长和方向比如倒序遍历、隔一个取一个// 倒序遍历从最后一个元素开始向前走到第 0 个for(intinums.Length-1;i0;i--){Console.WriteLine(nums[i]);}// 步长为 2只访问下标为偶数的元素for(inti0;inums.Length;i2){Console.WriteLine(nums[i]);}多维数组与交错数组// 二维数组矩阵用 GetLength(维度) 获取每一维的长度注意不是 Lengthint[,]matrix{{1,2,3},{4,5,6}};for(intr0;rmatrix.GetLength(0);r)// 行数{for(intc0;cmatrix.GetLength(1);c)// 列数{Console.Write(matrix[r,c] );}Console.WriteLine();}// 交错数组数组的数组每一维长度可以不同int[][]jagged{new[]{1,2},new[]{3,4,5}};foreach(int[]rowinjagged)// 外层遍历每一行行本身是一个数组{foreach(intvinrow)// 内层遍历该行的每个元素{Console.Write(v );}Console.WriteLine();}三、遍历常用集合除了数组实际开发中用得更多的是各种泛型集合它们都能用foreach直接遍历。3.1 ListTvarlistnewListstring{张三,李四,王五};// foreach 遍历foreach(stringnameinlist){Console.WriteLine(name);}// List 还提供了 ForEach 方法传入一个委托Lambda来处理每个元素list.ForEach(nameConsole.WriteLine(name));3.2 DictionaryTKey, TValue字典是键值对集合遍历时拿到的是KeyValuePairvardictnewDictionaryint,string{{1,一},{2,二},{3,三}};// 遍历键值对同时拿到 Key 和 Valueforeach(KeyValuePairint,stringkvindict){Console.WriteLine($键{kv.Key}值{kv.Value});}// 只遍历键foreach(intkeyindict.Keys){Console.WriteLine(key);}// 只遍历值foreach(stringvalueindict.Values){Console.WriteLine(value);}3.3 HashSetT、QueueT、StackT// HashSet元素不重复、无序varsetnewHashSetint{3,1,2,1};foreach(intxinset){Console.WriteLine(x);}// Queue 队列先进先出。注意foreach 只是查看不会把元素出队varqueuenewQueueint(new[]{1,2,3});foreach(intxinqueue){Console.WriteLine(x);}// Stack 栈后进先出。foreach 同样不会弹出元素varstacknewStackint(new[]{1,2,3});foreach(intxinstack){Console.WriteLine(x);}3.4 遍历字符串字符串本质是字符序列也可以foreachstringtextabc123;foreach(charchintext){Console.WriteLine(ch);// 依次输出 a b c 1 2 3}四、foreach 的本质IEnumerable 与 IEnumerator这是理解遍历的核心。为什么数组、List、字典、字符串都能用foreach因为它们都实现了IEnumerable可枚举接口这个接口承诺我能提供一个枚举器让你挨个访问我的元素。4.1 两个关键接口// IEnumerable可枚举对象负责提供一个枚举器publicinterfaceIEnumerableoutT:IEnumerable{IEnumeratorTGetEnumerator();// 返回一个枚举器}// IEnumerator枚举器负责真正地一步步移动、取出当前元素publicinterfaceIEnumeratoroutT:IDisposable,IEnumerator{TCurrent{get;}// 当前位置的元素}publicinterfaceIEnumerator{boolMoveNext();// 前进到下一个元素成功返回 true已到末尾返回 falsevoidReset();// 重置到起始位置实际很少用objectCurrent{get;}}可以这样理解二者关系IEnumerable是集合它能发给你一个游标枚举器IEnumerator是那个游标它有MoveNext()往后挪一格和Current当前格的内容。4.2 foreach 的等价展开我们写的foreach编译器会把它翻译成下面这样的代码简化版// 我们写的代码foreach(varitemincollection){Use(item);}// 编译器大致等价于using(varenumeratorcollection.GetEnumerator())// 1. 获取枚举器using 保证最后释放{while(enumerator.MoveNext())// 2. 不断往后移动{varitemenumerator.Current;// 3. 取出当前元素Use(item);// 4. 执行循环体}}// 5. 离开 using 时调用 Dispose 释放整个执行流程如下图这也解释了一个重要现象foreach只依赖GetEnumerator/MoveNext/Current根本不关心集合内部是数组、链表还是哈希表所以任何实现了IEnumerable的类型都能被foreach遍历。4.3 用 yield return 快速写迭代器如果想让自己的类型可被 foreach可以去实现IEnumerable但更简单的方式是用yield return——编译器会自动帮你生成枚举器// 返回 0 到 max 之间的所有偶数publicIEnumerableintGetEvenNumbers(intmax){for(inti0;imax;i2){// yield return返回一个元素然后方法在这里暂停// 下次调用 MoveNext 时再从这里继续往下执行yieldreturni;}}// 使用和遍历普通集合一模一样foreach(intninGetEvenNumbers(10)){Console.WriteLine(n);// 0 2 4 6 8 10}yield还有一个特点是延迟执行、按需生成IEnumerableintevensGetEvenNumbers(1000000);// 这一行执行后方法体其实还没有运行不会真的生成一百万个数字foreach(intninevens){Console.WriteLine(n);if(n10)break;// 只枚举到 10后面的数字根本不会被生成}五、遍历中的修改与删除高频踩坑5.1 foreach 中不能修改集合结构在foreach遍历期间对集合进行Add、Remove等改变结构的操作会抛出InvalidOperationException集合已修改枚举操作可能无法执行varlistnewListint{1,-2,3,-4,5};// 错误写法foreach 中删除元素运行时直接抛异常foreach(intxinlist){if(x0)list.Remove(x);// 不允许}原因枚举器内部记录了集合的版本号一旦集合结构变化、版本号对不上MoveNext()就会抛异常这是一种防止遍历错乱的保护机制。5.2 遍历中删除元素的正确方法varlistnewListint{1,-2,3,-4,5};// 方法一倒序 for RemoveAt。删除后后面的元素前移但因为是倒着走不会漏元素for(intilist.Count-1;i0;i--){if(list[i]0)list.RemoveAt(i);}// 方法二用 List 自带的 RemoveAll一行搞定最推荐用于 Listlist.RemoveAll(xx0);// 方法三先用 ToList() 复制一份快照遍历快照、删除原集合foreach(intxinlist.ToList()){if(x0)list.Remove(x);}注意正序for中直接RemoveAt会漏元素——删除后后面的元素整体前移而索引又i就会跳过紧挨着的下一个元素。这也是为什么推荐倒序删除。字典删除元素可用键集合快照的方式兼容性最好vardictnewDictionaryint,string{{1,a},{2,b}};// 先 ToList 复制键再遍历复制出来的键去删除foreach(intkeyindict.Keys.ToList()){if(key2)dict.Remove(key);}5.3 能不能修改元素本身要区分改集合结构和改元素内容varpeoplenewListPerson{newPerson{Name张三,Age18}};// 引用类型foreach 中修改元素的属性是允许的集合结构没变foreach(Personpinpeople){p.Age;// 可以// people.Add(new Person()); // 但不能新增/删除元素}// 值类型foreach 的迭代变量是只读副本不能给它赋值foreach(intninnew[]{1,2,3}){// n n 1; // 编译错误无法为 foreach 迭代变量赋值}六、遍历方式对比与选择遍历方式是否需要索引典型场景能否改变集合结构特点for是数组 / List需控制步长、方向或边遍历边改可以删除用倒序直观、性能好foreach否所有实现IEnumerable的集合不能最简洁、可读性好while否条件不固定、流读取、数据拉取手动管理最灵活do-while否循环体至少要执行一次手动管理先做后判断List.ForEach否对 List 每个元素执行一个委托不能函数式写法性能方面现代 .NET数组for和foreach性能基本一致。JIT 会把数组的foreach直接优化成索引访问没有额外开销ListTfor理论上略快但foreach用的是结构体枚举器、通常也会被内联日常开发差异可以忽略极致性能 / 大数量可考虑SpanT配合for减少接口调用和装箱泛型集合IEnumerableT遍历值类型不会装箱而老式非泛型集合如ArrayList遍历会有装箱开销新项目应优先使用泛型集合。选择建议只是依次处理集合元素、不需要索引 —— 优先foreach需要索引、要修改元素、控制步长或方向 —— 用for终止条件不固定如读流、轮询—— 用while。七、常见坑与注意事项foreach 中增删元素会抛InvalidOperationException删除用倒序for、RemoveAll或快照正序 for 删除漏元素删除导致元素前移、索引自增会跳过相邻元素改用倒序多维数组长度用错多维数组要用GetLength(0/1)Length返回的是元素总数对 null 使用 foreach集合为null时遍历会抛NullReferenceException必要时先判空IEnumerable 重复枚举LINQ 查询每次foreach都会重新执行必要时用ToList()固化结果误以为 foreach 能改迭代变量值类型迭代变量是只读副本改它没有意义也无法编译Queue/Stack 的 foreach 会移除元素吗不会枚举不改变集合出队/入栈要用对应方法非泛型集合的装箱开销优先使用ListT、IEnumerableT等泛型类型。八、小结遍历就是依次访问一组数据三要素是起点、前进规则、终止条件数组常用for可控步长/方向和foreach简洁多维数组用GetLength交错数组可嵌套遍历List、Dictionary、HashSet、Queue、Stack、string 都能用foreach字典遍历得到KeyValuePairforeach的本质是通过IEnumerable.GetEnumerator()拿到枚举器再循环调用MoveNext()、取Current最后Disposeyield return可以极简地实现迭代器且是延迟、按需执行foreach中不能增删集合元素删除元素推荐倒序for、RemoveAll或ToList快照遍历集合首选foreach需要索引和精细控制时用for条件不固定时用while。建议动手把本文每段代码敲一遍再试着用yield写一个自己的可遍历类型就能彻底把遍历这块知识掌握扎实。