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,每个分支必须以 breakreturnthrowgoto 结尾。
  • 多 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# 提供 forforeachwhiledo-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 并不是语法糖那么简单,它本质是对「迭代器」的标准化调用:
  1. 调用集合的 GetEnumerator() 方法,获取枚举器对象
  2. 循环调用枚举器的 MoveNext() 方法,返回 false 则结束循环
  3. 通过枚举器的 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 替代。
  • 不要用异常控制正常流程:异常仅用于处理错误情况,正常业务逻辑用条件判断实现。
  • 减少循环内重复计算:将循环中不变的计算提到循环外部,提升性能。
posted @ 2026-07-08 17:00  挺秃然的i  阅读(8)  评论(0)    收藏  举报