资讯动态

C#循环语句详解:while、for、foreach、do while选型与避坑指南

发布时间:2026/10/14 6:41:06 来源:尧图企业网站定制
循环这东西刚学编程那会儿我觉得自己懂了不就是重复干活嘛。结果第一次写批量数据处理一个while条件写反程序直接卡死风扇狂转我盯着屏幕愣是等了五分钟才反应过来要强制结束。后来带新人发现大家踩的坑惊人地相似for和foreach分不清什么时候该用哪个break和continue老是搞混嵌套循环一写就晕无限循环要么不敢碰要么一碰就翻车。这篇就把 C# 里这几类循环语句从头到尾捋一遍结合我自己踩过的坑和实际项目里的用法把每个细节讲透。不管你是刚接触编程的新手还是写了几年代码但循环这块总觉得不够扎实的老手应该都能从里面找到点有用的东西。1. 循环语句整体设计与选型思路1.1 为什么需要这么多种循环C# 提供了四种循环结构while、for、foreach、do while。很多新手会问功能都差不多为什么搞这么多花样其实每一种都有它最适合的场景选对了代码读起来顺畅选错了虽然也能跑但维护的人会骂人。我打个比方。for就像你已知要跑多少圈操场圈数写在计划表上while是你不知道要跑多久只知道“跑到累为止”do while是“先跑一圈再说跑完再看要不要继续”foreach则是“把篮子里每个苹果都拿出来擦一遍”。四种循环对应四种不同的思维模式选型的时候先问自己一个问题我是在按次数循环还是在按条件循环还是在遍历一个集合从底层实现来看for和while几乎可以互相转换编译器生成的中间代码差异很小。foreach稍微特殊一些它依赖集合类型的GetEnumerator方法编译后会展开成迭代器模式的调用。do while和while的唯一区别是条件判断的时机——一个在前一个在后。理解这些底层差异能帮你在性能敏感的场景做出更合适的选择。1.2 选型决策什么场景用哪种循环我整理了一个选型对照表这是我这些年实际写代码总结出来的判断逻辑循环类型适用场景典型例子不适用场景for循环次数已知或可计算遍历数组下标、执行固定次数操作遍历集合元素用 foreach 更清晰while循环次数未知依赖条件判断读取数据直到遇到结束标记已知次数的简单循环for 更紧凑do while至少执行一次再判断是否继续用户输入验证、菜单循环可能一次都不需要执行的场景foreach遍历实现了 IEnumerable 的集合遍历 List、数组、Dictionary需要在遍历中修改集合结构选型的时候有个简单的判断口诀次数确定用 for条件驱动用 while至少一次用 do while遍历集合用 foreach。这个口诀覆盖了日常开发中百分之九十以上的场景。不过实际项目里经常有例外。比如你需要遍历一个ListT但同时要拿到索引这时候用for反而比foreach方便因为foreach不直接暴露索引。再比如你需要倒序遍历for写起来更自然。所以口诀是起点不是铁律。1.3 循环控制语句的定位break和continue是两个循环控制关键字它们不构成循环本身但几乎所有的循环都离不开它们。break是“我不玩了”直接跳出当前循环continue是“这轮不算下一轮”跳过本次迭代剩余的代码。这两个关键字看起来简单但用不好会出大问题。我见过有人在嵌套循环里用break以为跳出了所有循环结果只跳出了最内层外层还在跑数据全乱了。也见过continue写在循环体最后一行等于什么都没做纯属多余。后面我会专门用一节来讲这两个关键字的正确用法和常见陷阱。2. 四种循环语句核心细节与实操要点2.1 while 循环条件驱动的重复执行while循环的语法结构非常简洁while (条件表达式) { // 循环体 }执行逻辑是先判断条件表达式如果为true就执行循环体执行完再回来判断条件如此反复直到条件为false时退出。注意这里的关键点——条件判断在循环体之前所以如果第一次判断就是false循环体一次都不会执行。while最适合的场景是“你不知道要循环多少次但你知道什么时候该停”。比如从网络流中读取数据直到遇到结束标志或者在一个游戏循环里持续处理输入直到玩家选择退出。int[] data { 3, 7, 1, 9, 4, 0, 6 }; int index 0; int sum 0; while (index data.Length data[index] ! 0) { sum data[index]; index; } Console.WriteLine($累加结果{sum}停止位置索引{index});这段代码从数组中累加元素遇到 0 或者到达数组末尾就停止。条件里用了短路运算符先判断索引是否越界再判断当前元素是否为零顺序不能反否则可能抛出索引越界异常。注意while循环最常见的错误是忘记在循环体内改变条件变量导致无限循环。写while的时候养成一个习惯——先确认循环体里有没有让条件趋向false的语句。2.2 for 循环精确控制的计数循环for循环把初始化、条件判断、迭代更新三件事写在一行里for (初始化; 条件; 迭代器) { // 循环体 }执行顺序是初始化只执行一次然后判断条件条件为true则执行循环体循环体结束后执行迭代器再判断条件如此反复。这个顺序一定要记牢尤其是迭代器在循环体之后执行这一点很多边界问题都跟这个有关。for循环最典型的用法是遍历数组string[] fruits { 苹果, 香蕉, 橙子, 葡萄, 西瓜 }; for (int i 0; i fruits.Length; i) { Console.WriteLine($第 {i 1} 个水果{fruits[i]}); }这里i的作用域仅限于循环内部循环结束后i就不再可访问了。这是 C# 和某些语言不一样的地方好处是避免了变量污染。for循环的灵活性很高初始化和迭代器部分可以写多个表达式用逗号分隔for (int i 0, j 10; i j; i, j--) { Console.WriteLine($i{i}, j{j}); }这种双向逼近的写法在某些算法里很有用比如判断回文字符串。实操心得for循环的条件部分尽量用而不是这是编程界的惯例。i array.Length比i array.Length - 1更直观也不容易在边界上出错。2.3 foreach 循环集合遍历的利器foreach是专门为遍历集合设计的foreach (元素类型 变量名 in 集合) { // 循环体 }它不需要你关心索引不需要知道集合长度编译器会自动调用集合的枚举器来逐个取出元素。任何实现了IEnumerable或IEnumerableT接口的类型都可以用foreach遍历包括数组、ListT、DictionaryTKey, TValue、HashSetT等等。Liststring cities new Liststring { 北京, 上海, 广州, 深圳 }; foreach (string city in cities) { Console.WriteLine(city); }foreach最大的优势是代码简洁、意图明确。你一眼就能看出这是在遍历集合不需要去分析索引变量的变化规律。在团队协作中能用foreach的地方尽量用foreach代码可读性会好很多。但foreach有一个重要的限制不能在遍历过程中修改集合的结构。比如在foreach里对List执行Add或Remove操作会抛出InvalidOperationException异常。这是因为枚举器在创建时会记录集合的版本号一旦检测到版本变化就会拒绝继续迭代。// 错误示范遍历时删除元素 Listint numbers new Listint { 1, 2, 3, 4, 5 }; foreach (int n in numbers) { if (n % 2 0) { numbers.Remove(n); // 这里会抛异常 } }如果确实需要在遍历时删除元素有两个方案一是用for循环倒序遍历二是用RemoveAll方法或者先收集要删除的元素再统一处理。// 方案一for 倒序遍历删除 for (int i numbers.Count - 1; i 0; i--) { if (numbers[i] % 2 0) { numbers.RemoveAt(i); } } // 方案二RemoveAll 一行搞定 numbers.RemoveAll(n n % 2 0);注意foreach遍历时虽然不能修改集合结构但可以修改元素的属性如果元素是引用类型。比如遍历一个对象列表修改每个对象的某个属性值是允许的。2.4 do while 循环至少执行一次的保证do while的语法是把条件判断放在循环体后面do { // 循环体 } while (条件表达式);注意末尾的分号不能省略这是初学者常犯的语法错误。执行逻辑是先执行一次循环体然后判断条件如果为true就回去再执行循环体直到条件为false。do while最适合的场景是“至少要做一次做完之后再决定要不要继续”。最典型的就是用户输入验证string input; do { Console.Write(请输入一个 1 到 100 之间的整数); input Console.ReadLine(); } while (!int.TryParse(input, out int result) || result 1 || result 100); Console.WriteLine($你输入的是{result});这段代码保证至少提示用户输入一次如果输入不合法就继续提示直到用户输入了合法的值为止。用while也能实现同样的功能但需要把提示语句写两遍或者在循环前先赋一个初始值不如do while来得自然。实操心得do while在实际项目中的使用频率远低于其他三种循环但在“输入验证”和“菜单循环”这两个场景里它是最优雅的选择。不要因为它用得少就忽略它。3. 循环控制与嵌套循环的实战应用3.1 break 与 continue 的正确用法break和continue是循环体内的两个控制流关键字它们改变循环的默认执行路径。break的作用是立即终止当前循环程序控制流跳到循环后面的第一条语句。它可以用在四种循环中的任何一种里。// 在数组中查找第一个大于 50 的数 int[] scores { 23, 45, 67, 89, 12, 56 }; int found -1; for (int i 0; i scores.Length; i) { if (scores[i] 50) { found scores[i]; break; // 找到了就退出不需要继续遍历 } } Console.WriteLine(found 0 ? $找到{found} : 未找到);continue的作用是跳过本次迭代中剩余的代码直接进入下一次迭代。注意它不会终止循环只是跳过当前这一轮。// 输出 1 到 20 之间的所有奇数 for (int i 1; i 20; i) { if (i % 2 0) { continue; // 偶数跳过不执行后面的输出 } Console.WriteLine(i); }这两个关键字用好了能让代码更简洁但滥用会让循环逻辑变得难以追踪。我的经验是一个循环体里break和continue加起来不要超过两个超过了就考虑重构把循环体拆成独立的方法。3.2 嵌套循环的执行逻辑与性能考量嵌套循环就是循环里面再套循环。外层循环每执行一次内层循环要完整地跑一遍。这意味着如果外层循环执行 M 次内层循环执行 N 次总的循环体执行次数是 M × N。// 打印九九乘法表 for (int i 1; i 9; i) { for (int j 1; j i; j) { Console.Write(${j}×{i}{i * j}\t); } Console.WriteLine(); }这段代码里外层i从 1 到 9内层j从 1 到i。总执行次数是 123...9 45 次。注意内层循环的条件用了j i而不是固定值这样每一行的列数随行数递增形成了三角形的输出效果。嵌套循环的性能问题在数据量大的时候会非常明显。三层嵌套循环每层循环 1000 次总执行次数就是十亿次。所以写嵌套循环之前先估算一下总迭代次数如果超过百万级别就要考虑有没有优化的空间。注意在嵌套循环中使用break只会跳出最内层循环外层循环不受影响。如果需要一次性跳出所有循环通常的做法是用一个标志变量或者把循环封装成方法后用return。// 用标志变量跳出多层循环 bool found false; for (int i 0; i 10 !found; i) { for (int j 0; j 10; j) { if (someCondition(i, j)) { found true; break; // 跳出内层 } } // 外层条件里的 !found 会阻止继续循环 }3.3 无限循环的写法与退出策略无限循环是指条件永远为true的循环。最常见的写法是while (true)也可以用for (;;)。while (true) { Console.Write(请输入命令输入 exit 退出); string command Console.ReadLine(); if (command exit) { break; // 唯一的退出通道 } Console.WriteLine($执行命令{command}); }无限循环本身不是 bug很多程序的主循环就是无限循环比如游戏引擎的渲染循环、服务器的消息处理循环。关键在于必须有一个明确的退出通道通常是break或者return。写无限循环的时候我习惯在循环体开头加一个注释标明退出条件是什么。这样后面维护的人一眼就能看到出口在哪里不会觉得这是个死循环。实操心得调试无限循环的时候如果程序已经卡死在控制台程序里可以按 CtrlC 强制终止。在 IDE 里调试时可以设置断点后单步执行观察循环变量的变化找到为什么条件没有趋向false。4. 常见问题与排查技巧实录4.1 循环相关异常速查表异常类型典型原因排查方法解决方案IndexOutOfRangeException循环变量越界检查循环条件是否用了而不是条件改为i array.LengthInvalidOperationExceptionforeach 中修改集合检查循环体内是否有 Add/Remove改用 for 倒序或 RemoveAllNullReferenceException遍历 null 集合检查集合是否已初始化循环前加 null 判断程序卡死无响应无限循环检查循环变量是否在变化确保循环体改变条件变量StackOverflowException递归替代循环检查是否有方法自调用改用循环结构替代递归4.2 循环变量作用域的坑C# 中for循环的循环变量作用域仅限于循环内部这一点和 JavaScript 的var不同。但有一个容易忽略的细节在 C# 5 之前foreach的迭代变量是共享的闭包捕获会出现问题。C# 5 之后每次迭代都会创建新的变量这个问题已经修复了。// C# 5 中这段代码输出 0 1 2符合预期 var actions new ListAction(); for (int i 0; i 3; i) { actions.Add(() Console.WriteLine(i)); } foreach (var action in actions) { action(); }但如果你在for循环里用闭包捕获循环变量结果可能和预期不一样。因为for循环的变量i在整个循环中是同一个变量闭包捕获的是变量的引用而不是值。解决方法是把循环变量复制到一个局部变量里。var actions new ListAction(); for (int i 0; i 3; i) { int captured i; // 复制一份 actions.Add(() Console.WriteLine(captured)); }4.3 性能优化什么时候该避免循环循环是程序中最常见的性能瓶颈来源之一。以下几种情况需要特别注意第一种是循环体内做了重复的计算。比如for (int i 0; i list.Count; i)如果list.Count在循环中不会变化应该在循环外先存到一个变量里。虽然现代编译器有时会做优化但显式写出来更稳妥。第二种是嵌套循环中的字符串拼接。字符串是不可变的每次拼接都会创建新对象。在循环里拼接字符串应该用StringBuilder。// 不推荐 string result ; for (int i 0; i 10000; i) { result i.ToString(); } // 推荐 var sb new StringBuilder(); for (int i 0; i 10000; i) { sb.Append(i); } string result sb.ToString();第三种是可以用 LINQ 替代的循环。很多情况下foreach循环做的事情可以用 LINQ 的Where、Select、Sum等方法一行搞定代码更简洁可读性更好。当然 LINQ 有它自己的性能开销在极端性能敏感的场景还是得用循环。4.4 调试循环问题的实用技巧调试循环问题的时候我常用的几个手段第一在循环体开头打印循环变量的值。这是最原始但也最有效的方法能快速看出循环变量的变化是否符合预期。第二用条件断点。在 IDE 里给断点设置条件比如i 50这样程序只在特定迭代时停下来不用手动跳过前 49 次。第三给循环加一个安全计数器。在调试阶段如果怀疑可能是无限循环可以加一个计数器超过一定次数就强制退出并打印日志。int safetyCounter 0; while (someCondition) { if (safetyCounter 100000) { Console.WriteLine(循环次数异常强制退出); break; } // 正常循环体 }这个技巧在排查生产环境的偶发问题时特别有用能防止程序因为意外情况陷入死循环。4.5 循环写法的代码风格建议最后分享几条我在团队里推行的循环写法规范都是实际项目中总结出来的能用foreach的地方优先用foreach代码意图更清晰for循环的条件统一用或不用或配合加减一循环嵌套不超过三层超过三层考虑提取方法或重构数据结构while循环体内必须有改变条件的语句且尽量放在显眼位置无限循环必须用while (true)而不是for (;;)前者意图更明确break和continue不要嵌套使用一个循环里最多各出现一次这些规范不是死规定但遵循它们能让代码的可读性和可维护性提升一个档次。尤其是团队协作的时候统一的风格能减少很多沟通成本。我在实际项目中最大的体会是循环写得好不好很大程度上反映了一个程序员对边界条件的敏感度。循环的起始值、终止条件、迭代步长这三个要素任何一个出问题都会导致 bug。每次写完循环我都会在脑子里过一遍第一次迭代会发生什么最后一次迭代会发生什么如果集合为空会怎样把这三个问题想清楚了循环基本就不会出错了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑