C#基础(四):流程控制
流程控制语句用于决定程序代码的执行顺序与分支走向,是构建业务逻辑的基础。C# 的流程控制体系分为选择分支、循环迭代、跳转语句三大类,并且在现代 C# 中通过模式匹配、switch 表达式、迭代器等特性大幅增强了表达能力。本文从语法规则、执行原理、高级特性、常见陷阱四个维度展开讲解。
一、选择语句(分支结构)
根据条件判断结果,选择执行不同的代码分支,核心分为 if-else 系列和 switch 系列。
1.1 if-else 语句
1.1.1 基础语法与执行逻辑
if 是最基础的分支语句,条件为 true 时执行对应代码块,else 提供默认分支。
// 单分支 int score = 85; if (score >= 60) { Console.WriteLine("及格"); } // 双分支 if (score >= 60) { Console.WriteLine("及格"); } else { Console.WriteLine("不及格"); } // 多分支:else if 链式判断,从上到下依次匹配,命中一个后后续分支不再执行 if (score >= 90) { Console.WriteLine("优秀"); } else if (score >= 80) { Console.WriteLine("良好"); } else if (score >= 60) { Console.WriteLine("及格"); } else { Console.WriteLine("不及格"); }
1.1.2 嵌套 if 与「悬空 else」陷阱
if 可以多层嵌套,但如果省略大括号,会出现就近匹配原则导致的「悬空 else」问题,极易产生逻辑错误。
int x = -1, y = 2; // ❌ 缩进误导:else 实际匹配最近的内层 if,而非外层 if if (x > 0) if (y > 0) Console.WriteLine("都为正数"); else Console.WriteLine("x 不是正数"); // 上述代码不会输出任何内容,因为 x=-1 不进入外层 if,else 属于内层 if // ✅ 正确写法:始终使用大括号明确作用域 if (x > 0) { if (y > 0) { Console.WriteLine("都为正数"); } } else { Console.WriteLine("x 不是正数"); }
最佳实践:无论代码块有多少行,都建议加上大括号,避免缩进歧义与后续维护时引入 bug。
1.1.3 常见坑与最佳实践
1.不要将赋值运算符 = 当作相等判断 ==
bool flag = false; // if (flag = true) { } // 编译通过,但实际是赋值操作,永远为 true if (flag == true) { } // 正确写法 if (flag) { } // 更简洁的布尔判断,推荐写法
2.浮点数不要直接用 == 判断相等,应通过差值小于阈值判断
3.空值判断优先使用 is not null / is null,语义更清晰
string? str = GetString(); if (str is not null) // 推荐 C# 9.0+ 写法 { Console.WriteLine(str.Length); }
1.2 switch 语句(传统语句形式)
switch 适合多分支常量匹配场景,比多层 else if 结构更清晰、性能更优。
1.2.1 基础语法
int day = 3; switch (day) { case 1: Console.WriteLine("周一"); break; // 必须显式跳出,不能隐式贯穿 case 2: Console.WriteLine("周二"); break; case 3: case 4: case 5: Console.WriteLine("工作日"); break; default: Console.WriteLine("周末"); break; }
1.2.2 核心规则
- 禁止隐式贯穿:C# 不允许 case 执行完后自动流入下一个 case,每个分支必须以
break、return、throw或goto结尾。 - 多 case 共享代码:多个 case 标签连续写在一起,可共用同一段逻辑。
- default 可选:所有 case 都不匹配时执行 default,位置不固定但通常放最后。
- 传统 switch 限制:只能匹配常量值,支持类型仅限整数、字符、字符串、枚举、布尔等基元类型。
1.2.3 显式贯穿:goto case
如果需要主动跳转到指定 case,可以使用 goto case 实现显式贯穿。
int num = 2; switch (num) { case 1: Console.WriteLine("case 1"); break; case 2: Console.WriteLine("case 2"); goto case 1; // 执行完 case 2 后跳转到 case 1 default: break; } // 输出: // case 2 // case 1
1.3 switch 模式匹配增强(C# 7.0+)
从 C# 7.0 开始,switch 突破了「常量匹配」的限制,支持类型模式、属性模式、关系模式等,能力大幅提升。
1.3.1 类型模式
根据变量的运行时类型进行分支匹配,匹配成功后自动转换为对应类型。
object obj = "hello world"; switch (obj) { case string s: Console.WriteLine($"字符串,长度:{s.Length}"); break; case int i when i > 0: // 配合 when 条件子句 Console.WriteLine($"正整数:{i}"); break; case int i: Console.WriteLine($"非正整数:{i}"); break; case null: Console.WriteLine("空值"); break; default: Console.WriteLine("未知类型"); break; }
1.3.2 when 条件子句
在 case 匹配的基础上增加额外的逻辑条件,实现更精细的分支控制。
int score = 88; switch (score) { case int s when s >= 90: Console.WriteLine("优秀"); break; case int s when s >= 60: Console.WriteLine("及格"); break; default: Console.WriteLine("不及格"); break; }
1.3.3 属性模式(C# 8.0+)
直接匹配对象的属性值,无需提前拆包。
Person person = new Person { Age = 22, Name = "Tom" }; switch (person) { case { Age: > 18, Name: "Tom" }: Console.WriteLine("成年的 Tom"); break; case { Age: < 18 }: Console.WriteLine("未成年人"); break; case null: Console.WriteLine("空对象"); break; }
1.3.4 关系与逻辑模式(C# 9.0+)
支持 > < >= <= 关系运算符和 and or not 逻辑运算符。
int age = 25; switch (age) { case < 0: Console.WriteLine("年龄非法"); break; case >= 0 and < 18: Console.WriteLine("未成年人"); break; case >= 18 and < 60: Console.WriteLine("成年人"); break; case >= 60: Console.WriteLine("老年人"); break; }
1.4 switch 表达式(C# 8.0+)
switch 表达式是现代 C# 最常用的特性之一,将 switch 从「语句」升级为「表达式」,直接返回结果值,语法更简洁、表达力更强。
1.4.1 核心特点
- 使用
=>定义分支,替代case:和break - 必须覆盖所有可能的输入,通常用弃元
_作为默认分支 - 是表达式,有返回值,可直接赋值给变量
- 支持所有模式匹配语法
1.4.2 基础用法
int day = 3; string result = day switch { 1 => "周一", 2 => "周二", 3 or 4 or 5 => "工作日", 6 or 7 => "周末", _ => "无效日期" // 默认分支,弃元 };
1.4.3 元组模式:多参数同时匹配
通过元组同时匹配多个输入变量,替代复杂的 if-else 嵌套。
// 根据角色和操作判断是否有权限 bool CheckPermission(string role, string operation) { return (role, operation) switch { ("admin", _) => true, // 管理员所有操作都有权限 ("user", "read") => true, ("user", "write") => false, (_, _) => false }; }
1.4.4 位置模式:匹配解构对象
如果类型支持解构,可以直接匹配其解构值。
public record Point(int X, int Y); string GetPointQuadrant(Point p) => p switch { (0, 0) => "原点", ( > 0, > 0) => "第一象限", ( < 0, > 0) => "第二象限", ( < 0, < 0) => "第三象限", ( > 0, < 0) => "第四象限", _ => "坐标轴上" };
选型建议:
-
单值映射、多条件匹配优先用 switch 表达式,代码简洁易读
-
分支内包含多句复杂逻辑、副作用操作,用 switch 语句
-
2~3 个简单分支用
if-else即可,过度使用 switch 反而增加复杂度
二、循环语句
重复执行某段代码,直到满足终止条件。C# 提供 for、foreach、while、do-while 四种循环结构。
2.1 for 循环
适合循环次数已知的场景,结构紧凑,将初始化、条件、增量集中在一行。
2.1.1 执行流程
- 执行初始化语句(仅执行一次)
- 判断循环条件,为
false则退出循环 - 条件为
true,执行循环体 - 执行增量语句
- 回到第 2 步重复判断
// 基础:输出 0~9 for (int i = 0; i < 10; i++) { Console.WriteLine(i); } // 嵌套循环:九九乘法表 for (int i = 1; i <= 9; i++) { for (int j = 1; j <= i; j++) { Console.Write($"{j}*{i}={i * j}\t"); } Console.WriteLine(); }
2.1.2 灵活写法
for 循环的三个部分都可以省略,省略条件时默认为 true,形成无限循环。
// 无限循环,等效于 while(true) for (;;) { // 循环体 }
2.1.3 经典坑:循环变量闭包陷阱
在循环中创建 lambda 表达式或匿名方法时,捕获的是循环变量本身,而非当前值,循环结束后所有委托都会拿到最终值。
var actions = new List<Action>(); // ❌ 错误写法:所有委托捕获同一个 i,循环结束 i=3 for (int i = 0; i < 3; i++) { actions.Add(() => Console.WriteLine(i)); } foreach (var act in actions) act(); // 输出 3 3 3 // ✅ 正确写法:循环内声明临时变量,每次循环捕获不同副本 for (int i = 0; i < 3; i++) { int temp = i; actions.Add(() => Console.WriteLine(temp)); } foreach (var act in actions) act(); // 输出 0 1 2
注意:
foreach循环在 C# 5.0 之后已修复此问题,每次迭代的循环变量是独立副本,不会出现闭包问题。
2.2 foreach 循环
专门用于遍历集合元素,是开发中使用频率最高的循环,底层基于迭代器模式实现。
2.2.1 基础用法
int[] nums = { 1, 2, 3, 4, 5 }; foreach (int num in nums) { Console.WriteLine(num); }
2.2.2 底层原理:迭代器模式
foreach 并不是语法糖那么简单,它本质是对「迭代器」的标准化调用:- 调用集合的
GetEnumerator()方法,获取枚举器对象 - 循环调用枚举器的
MoveNext()方法,返回false则结束循环 - 通过枚举器的
Current属性获取当前元素
等效的手写代码:
List<int> list = new List<int> { 1, 2, 3 }; using var enumerator = list.GetEnumerator(); while (enumerator.MoveNext()) { int item = enumerator.Current; Console.WriteLine(item); }
因此,只要一个类型包含 GetEnumerator() 方法、返回值包含 MoveNext() 和 Current,就可以用 foreach 遍历,不强制要求实现 IEnumerable 接口(鸭子类型)。
2.2.3 核心限制:循环中禁止修改集合
迭代器在遍历过程中会校验集合版本,如果遍历过程中对集合进行 Add/Remove 操作,会抛出 InvalidOperationException: 集合已修改,可能无法执行枚举操作。
var list = new List<int> { 1, 2, 3 }; // ❌ 运行时抛出异常 foreach (var item in list) { if (item == 2) list.Remove(item); } // ✅ 解决方案1:用 for 倒序遍历 for (int i = list.Count - 1; i >= 0; i--) { if (list[i] == 2) list.RemoveAt(i); } // ✅ 解决方案2:先收集要删除的元素,遍历结束后统一删除 var toRemove = list.Where(x => x == 2).ToList(); foreach (var item in toRemove) { list.Remove(item); }
2.2.4 ref 迭代(C# 7.3+)
对于值类型集合,可以通过 ref 直接修改集合内的元素,避免值拷贝。
int[] arr = { 1, 2, 3 }; foreach (ref int item in arr.AsSpan()) { item *= 2; // 直接修改数组原元素 } // arr 变为 { 2, 4, 6 }
2.3 while 循环
适合循环次数不确定的场景,先判断条件,再执行循环体,可能一次都不执行。
int count = 0; while (count < 5) { Console.WriteLine(count); count++; }
2.4 do-while 循环
先执行循环体,再判断条件,循环体至少执行一次。适合需要先执行一次再判断的场景(如用户输入校验)。
string? input; do { Console.Write("请输入内容:"); input = Console.ReadLine(); } while (string.IsNullOrWhiteSpace(input)); // 至少要求用户输入一次,空内容则重新输入
2.5 无限循环
常用写法有两种,功能完全等效:
// 写法1:可读性更好,推荐 while (true) { // 循环体,通过 break/return 退出 } // 写法2:for 省略所有部分 for (;;) { // 循环体 }
适用场景:后台服务主循环、消息消费、游戏主循环等长期运行的程序。
三、跳转语句
主动改变程序执行流程,跳转到指定位置。
3.1 break
- 作用:跳出最近的一层循环或 switch 语句,终止当前循环的执行。
- 注意:嵌套循环中,
break只能跳出所在的内层循环,无法直接跳出外层循环。
for (int i = 0; i < 5; i++) { if (i == 3) break; // 遇到 3 直接终止整个循环 Console.WriteLine(i); } // 输出:0 1 2
3.2 continue
- 作用:跳过当前循环的剩余代码,直接进入下一次条件判断。
- 与 break 区别:break 终止整个循环,continue 只跳过当前这一轮。
for (int i = 0; i < 5; i++) { if (i == 2) continue; // 跳过 2,进入下一轮 Console.WriteLine(i); } // 输出:0 1 3 4
3.3 return
- 作用:立即终止当前方法的执行,返回方法调用处;如果方法有返回值,需带回返回结果。
- 在循环中使用
return会直接跳出所有循环并结束方法。
int FindFirstNegative(int[] arr) { foreach (int num in arr) { if (num < 0) return num; // 找到第一个负数,直接返回,结束整个方法 } return 0; }
3.4 goto
跳转到代码中指定的标签位置,是最灵活但也最容易破坏代码结构的跳转语句。
基础语法
int i = 0; start: // 标签 Console.WriteLine(i); i++; if (i < 3) goto start; // 跳转到标签处
合法场景:跳出多层循环
嵌套循环中,break 只能跳出一层,使用 goto 可以一次性跳出所有循环,这是 goto 为数不多的合理使用场景。
for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { if (i * j > 20) goto EndLoop; // 直接跳出两层循环 } } EndLoop: Console.WriteLine("循环结束");
最佳实践:绝大多数场景下应避免使用 goto,多层循环可以通过封装方法 + return 替代,代码结构更清晰。
3.5 throw
抛出异常,中断正常的执行流程,跳转到最近的 catch 异常处理块。严格来说属于异常处理范畴,但本质也是流程控制的一种。
public void SetAge(int age) { if (age < 0 || age > 150) throw new ArgumentOutOfRangeException(nameof(age), "年龄不合法"); // 正常逻辑 }
四、迭代器与 yield(流程控制进阶)
yield 是 C# 中非常强大的特性,用于自定义迭代逻辑,编译器会自动生成状态机,实现延迟执行。
4.1 yield return:逐个返回元素
yield return 不会立即结束方法,而是返回当前元素并记录位置,下一次迭代时从当前位置继续执行。
// 生成指定范围内的偶数序列 IEnumerable<int> GetEvenNumbers(int max) { for (int i = 0; i <= max; i++) { if (i % 2 == 0) yield return i; // 逐个返回元素 } } // 使用 foreach (int num in GetEvenNumbers(10)) { Console.WriteLine(num); }
核心特性:延迟执行
调用包含 yield return 的方法时,方法体不会立即执行,只有当开始遍历(调用 MoveNext())时,代码才会逐步执行。
var evens = GetEvenNumbers(10); // 此时方法体并未执行 // 开始 foreach 遍历时才真正逐次执行方法内逻辑
4.2 yield break:提前终止迭代
主动结束迭代,后续不再返回任何元素。
IEnumerable<int> ReadNumbersUntilNegative() { while (true) { int num = int.Parse(Console.ReadLine()!); if (num < 0) yield break; // 遇到负数终止迭代 yield return num; } }
适用场景:生成无限序列、按需加载数据、简化状态机实现等。LINQ 的大部分标准查询运算符底层都是基于迭代器和延迟执行实现的。
五、最佳实践与常见坑点总结
- 分支选型:2~3 个简单分支用
if-else;多常量 / 模式匹配优先用switch表达式;复杂多条件逻辑避免多层嵌套,尽早 return 减少缩进。 - 循环选型:已知次数用
for;遍历集合用foreach;先执行后判断用do-while。 - 遍历修改集合:foreach 中不要增删集合元素,改用 for 倒序遍历或先收集后批量处理。
- 闭包陷阱:for 循环中使用 lambda 注意捕获变量问题,foreach 循环无需担心。
- 大括号规范:if/for/foreach 即使只有一行代码也建议加大括号,避免维护时引入悬空 else 等问题。
- 避免 goto 滥用:除了跳出多层循环等极端场景,尽量不用 goto,优先用方法封装 + return 替代。
- 不要用异常控制正常流程:异常仅用于处理错误情况,正常业务逻辑用条件判断实现。
- 减少循环内重复计算:将循环中不变的计算提到循环外部,提升性能。

浙公网安备 33010602011771号