>> >> >> Reference << << << <<<<<<Ref>>>>>>
Control Blocks
Modified: 2025-06-01 | Author:ljf12825

while / do while

while

用于事前检查型循环,常见于:

while (condition)
{
	// loop body
}
  1. 先判断条件
  2. 条件为true -> 执行循环体
  3. 条件为false -> 直接跳出循环

do while

用于必须先执行依次的场景,例如

do
{
	// loop body
}
while (condition)

执行逻辑:

  1. 先执行循环体一次
  2. 再判断条件
  3. 条件为true -> 继续循环
  4. 否则跳出

for, foreach, GetEnumerator()

for

最原始,最接近机器模型的循环
语法本质

for (init; condition; iterator)
{
	body
}

这是纯语法结构,不是方法,也不是接口驱动的
编译器做的事情非常直接

init;
while (condition)
{
	body;
	iterator;
}
核心特性
性能与内存视角
for (int i = 0; i < arr.Length; i++)
{
	sum += arr[i];
}

JIT在这里可以做到

这也是为什么数值计算、引擎代码、热路径几乎只用for

foreachGetEnumerator

foreach (var x in collection)
{
	body;
}

foreach是编译器级别的语法糖,核心依赖GetEnumerator()协议,但它不是简单地调用接口

foreach查找规则

注意:foreach不要求类型实现IEnumerable接口**
编译器在看到 foreach (var x in expr) 时,会按顺序尝试

1. 是否存在可访问的实例方法
GetEnumerator()

返回的类型(Enumerator)必须满足

不要求接口,不要求继承,只要“形状匹配”,即duck typing,但发生在编译期

2. 如果没有实例方法

尝试扩展方法GetEnumerator()

3. 如果都没有

才退化为

一个“非接口”的枚举器例子
struct MyCollection
{
	public Enumerator GetEnumerator() => new Enumerator();

	public struct Enumerator
	{
		int i;

		public int Current => i;

		public bool MoveNext()
		{
			i++;
			return i < 10;
		}
	}
}

完全合法的foreach对象,没有接口,没有继承,没有装箱,编译器在编译期通过形状匹配识别枚举器

编译器对foreach的处理
  1. 源代码
foreach (var x in collection)
{
	Do(x);
}
  1. 编译器展开(概念等价)
var enumerator = collection.GetEnumerator();
try
{
	while (enumerator.MoveNext())
	{
		var x = enumerator.Current;
		Do(x);
	}
}
finally
{
	if (enumerator is IDisposable d)
		d.Dispose();
}
GetEnumerator()
基本概念

GetEnumerator()是一个方法,用来获取集合的枚举器(Enumerator)。枚举器是对象或结构体,负责在循环中提供“当前元素”和迭代状态

List<int> list = new() { 1, 2, 3 };
var enumerator = list.GetEnumerator();
while (enumerator.MoveNext())
{
	int x = enumerator.Current;
	Console.WriteLine(x);
}
语言层定义

在C#中,GetEnumerator()有两种常见签名

  1. 非泛型版本(IEnumerable
IEnumerator IEnumerable.GetEnumerator();

返回IEnumerator,只提供object Current

  1. 泛型版本(IEnumerable<T>
IEnumerator<T> IEnumerable<T>.GetEnumerator();

返回IEnumerator,提供类型安全的T Current

GetEnumerator()与枚举器协议

枚举器协议:C#编译器规定,只要返回对象满足以下条件就可以用于foreach

不要求实现IEnumerator接口,只要“形状匹配”即可

GetEnumerator()的两种实现方式
  1. 类似数组List<T>的结构体枚举器
    • 返回struct,避免装箱
    • JIT可内联MoveNext()Current,性能接近for
    • 保存索引和版本号
public struct Enumerator
{
    private List<T> _list;
    private int _index;
    private int _version;
    public T Current => _list[_index];
    public bool MoveNext() { ... }
}
  1. yield / iterator 生成器
    • 编译器自动生成 state machine类
    • GetEnumerator()返回class类型的枚举器
    • MoveNext内部控制状态机
    • 支持延迟计算
IEnumerable<int> Numbers()
{
	yield return 1;
	yield return 2;
}

实际生成

private sealed class NumbersIterator : IEnumerable<int>, IEnumerator<int>
{
	int state;
	int current;
	public bool MoveNext() { ... }
	public int Current => current;
	public IEnumerator<int> GetEnumerator() => this;
}
GetEnumerator存在的意义

回到最开始的问题,foreach不要求类型实现IEnumerable接口,可以通过GetEnumerator,而不是只靠接口
原因只有一个:性能 + 表达力

这是高性能场景无法接受的
所有C#选择:编译期协议,而非运行时

值类型枚举器 vs 接口枚举器

为什么List<T>.foreach很快

foreach (var x in list)

因为

慢的情况

IEnumerable<int> e = list;
foreach (var x in e)

此时

只读遍历

这是一个语言设计层面的“硬约束”问题,C#无法、也不愿意,为foreach保证“可写语义”在所有枚举器模型下成立

foreach (var x in list)
{
	x = 10 ; // 编译错误
}

foreach (int x in array)
{
	x++; // 不影响原数组
}

原因不完全相同但本质一致

foreach的核心语义

编译器对foreach的展开中

var x = enumerator.Current;

Current是一个属性,属性返回值,不是可写引用。foreach根本不知道你枚举的是什么东西

GetEnumerator协议只要求

bool MoveNext();
T Current { get; }

因此,语言层面只能承诺:可以读到一个元素,而不是可以修改集合中的这个位置

为什么不能设计成“可写foreach”
  1. 因为很多集合根本不存在“可写位置”
IEnumerable<int> Numbers()
{
	yield return 1;
	yield return 2;
}

这里的Current不是数组元素,不是内存位置,是状态机计算出来的临时值

  1. 即使是数组,也存在歧义
foreach (var x in GetArray())
{ 
	x = 10;
}

GetArray()是什么?未知

值类型枚举器

常见枚举器是struct

List<T>.Enumerator : struct

如果允许

foreach (var x in list)
{
	x = ...
}

那等价于

enumerator.Current = ...

但问题是

如果强行支持,会导致灾难性不一致:值类型可改,引用类型不可改,这在语言设计上是不可接受的
C#的设计原则是:宁可统一禁止,也不制造“看起来能用、但实际上不可靠”的语义

foreach中允许修改对象的字段
foreach (var obj in list)
{
	obj.Value = 10; // 合法
}

obj是引用的拷贝,拷贝的是地址,修改的是对象本体;没有修改“枚举变量”,而是修改它指向的对象,这和foreach的只读规则不冲突

ref foreach

C# 7.3 引入特例

foreach (ref var x in array)
{
	x = 10;
}

这里成立是因为

这是专门为数组和Span设计的“受限逃生口”

为什么不能对所有IEnumerable生效?
因为

这是不可接收的API破坏

版本检查
定义
作用

检测枚举器在枚举期间是否被修改

运行机制

枚举开始前保存枚举器开始时的集合版本,每次MoveNext()检查_version是否匹配,不匹配则抛出异常

public class List<T> : IEnumerable<T>
{
    private int _version;  // 每次修改集合时递增
    private T[] _items;
    
    public void Add(T item)
    {
        // ... 添加逻辑
        _version++;  // 修改后版本号递增
    }
    
    public struct Enumerator
    {
        private readonly List<T> _list;
        private int _index;
        private readonly int _version;  // 保存迭代开始时的版本号
        
        public Enumerator(List<T> list)
        {
            _list = list;
            _index = 0;
            _version = list._version;  // 捕获当前版本
        }
        
        public bool MoveNext()
        {
            // 关键检查!
            if (_version != _list._version)
            {
                throw new InvalidOperationException(
                    "Collection was modified; enumeration operation may not execute.");
            }
            
            // ... 正常的迭代逻辑
        }
    }
}
List<int> list = new List<int> { 1, 2, 3 };

foreach (var item in list) // 创建枚举器:_version = 1
{
	Console.WriteLine(item); // MoveNext() 检查:1 == 1
}
// 顺利执行完成
List<int> list = new List<int> { 1, 2, 3 };

foreach (var item in list) // 创建枚举器 _version = 1
{
	Console.WriteLine(item);
	if (item == 1)
	{
		lsit.Add(4); // 修改集合:_version = 2
	}
	// 下次 MoveNext() 检查失败,抛出异常
}
// InvalidOperationException

值类型(struct)下的行为不同点

foreach中结构体是拷贝副本

struct MyStruct
{
  public int X;
}

Mystruct[] arr = { new MyStruct { X = 1 } };

foreach (var s in arr) s.X = 999; // 改变的是副本原数组不会改变

for中是通过索引器访问,直接修改原始结构体

for (int i = 0; i < arr.Length; ++i) arr[i].X = 999; // 真正修改了数组中的元素

适用场景

if

基本结构

if是C#中最常见的条件分支语句

if (condition expr)
{
	// 条件为 true 时执行的代码
}

可以加上

if (expr1)
{
	// expr1 成立
}
else if (expr2)
{
	// expr2 成立
}
else
{
	// 前面的都不成立
}

条件表达式的要求

C#中if后面必须是一个布尔类型表达式(bool
不像C/C++那样可以把int当成真假

int x = 1;

// if (x) // Error: Cannot implicitly convert type 'int' to 'bool'

if (x != 0)
{
	Console.WriteLine("x is not zero");
}

这体现了C#更强的类型安全设计理念

简写与嵌套

  1. 单行简写 如果只有一条语句,可以省略花括号(但不推荐)
if (score > 90)
	Console.WriteLine("Excellent");
  1. 嵌套if
if (age > 18)
{
	if (hasID)
	{
		Console.WriteLine("Access granted");
	}
}

嵌套太多会让逻辑混乱,建议使用&&||合并逻辑或提取函数

进阶写法

  1. 条件表达式(三元运算符)
string msg = (score >= 60) ? "及格" : "不及格";

相当于

if (score >= 60)
	msg = "及格";
else 
	msg="不及格";
  1. ifvar结合
if (int.TryParse(input, out var number))
	Console.WriteLine($"输入了数字 {number}");
else Console.WriteLine("输入无效");

这里的number只在if的作用域内有效

常见陷阱

  1. 多行省略花括号陷阱
if (flag)
	Console.WriteLine("Yes");
	Console.WriteLine("No"); // 这行总会执行
  1. 混用===
if (a = true) // 编译错误

C#特意设计为错误,防止C风格的低级错误

if是控制流的最小单元,但成熟的代码尽量减少显式if

switch

基础语法(C#早期版本)

switch (day)
{
	case 1:
		Console.WriteLine("Monday");
		break;
	
	case 2:
		Console.WriteLine("Tuesday");
		break;
	
	default:
		Console.WriteLine("Unknown");
		break;
}

字符串与枚举支持

C#从早期版本就支持

switch (command)
{
	case "start":
		Start();
		break;
	case "stop":
		Stop();
		break;
}

枚举更常见

enum Direction { Up, Down, Left, Right }

switch (dir)
{
	case Direction.Up:
		MoveUp();
		break;
}

C#7+模式匹配

C# 7开始,switch不再局限于“相等比较”,而是能做类型匹配与条件判断

object obj = 42;

switch (obj)
{
	case int i when i > 0:
		Console.WriteLine($"正整数: {i}");
		break;
	case string s:
		Console.WriteLine($"字符串: {s}");
		break;
	case null:
		Console.WriteLine($"null 值");
		break;
	default:
		Console.WriteLine($"未知类型");
		break;
}

这里引入了两个关键点

  1. 类型模式(case int i) 自动检测类型并声明局部变量
  2. 条件保护(when) 让case可以附加逻辑判断

C# 8+ 新写法:switch 表达式

switch不再是语句(statement),而是表达式(expression)

string GetDayType(int day) =>
	day switch
	{
		1 or 2 or 3 or 4 or 5 => "工作日",
		6 or 7 => "周末"
		_ => "未知"
	};
语句(statement)与表达式(expression)
概念定义特点结果
表达式(Expression)能计算出一个值的代码单元有值、有类型、可嵌套产生一个结果
语句(Statement)执行某种操作或行为的代码单元无值、控制流程、作用于状态不产生值(或值被丢弃)

表达式是“有结果的东西”,语句是“做事的东西”

int x = 3 + 4;
x = y * 2;
if (x > 0)
	y = 1;
  1. 表达式可以嵌套、组合
int z = (x + y) * max(3, f(2));

整个都是表达式,嵌套了函数调用、算术运算、常量等

  1. 语句控制执行顺序
if (...) { ... }
for (...) { ... }
return ...;

它们决定代码“什么时候做什么事”,但自身不返回值

  1. 部分语言模糊两者界限

现代语言设计趋向于“让语句表达式化”,这样可以写出更简洁的代码

C# 9~12的进一步强化

  1. 关系模式
string GetTempDesc(double t) => t switch
{
	< 0 => "冰点以下",
	>= 0 and < 30 => "正常",
	>= 30 => "高温",
};
  1. 属性模式
Person p = new Person { Name = "Tom", Age = 20 };

var type = p switch
{
	{ Age: < 18 } => "未成年",
	{ Age: >= 18 and < 65 } => "成年人",
	{ Age: >= 65 } => "老年人",
}
  1. 嵌套解构模式
var result = point switch
{
	{ X: 0, Y: 0 } => "原点",
	{ X: > 0, Y: > 0 } => "第一象限",
	_ => "其他", 
};

这些新特性让switch变成一种模式匹配引擎,而不仅仅是分支语句

常见陷阱

  1. 忘了break(老语法会导致穿透)
  2. 混用值类型和引用类型导致匹配失败
  3. 早期C#不支持多个case合并,现在可用or

核心哲学

C#的switch从命令式走向声明式,从“判断谁”变成“描述模式”
这种变化的本质,是把控制流逻辑结构化、数据化、函数化