迭代器是C#中一个非常强大且优雅的特性,它极大地简化了集合枚举的实现
概念
- 核心概念:迭代器是一种方法、get访问器或运算符,它使你能够在类或集合中支持
foreach循环 - 它的作用:它不会一次性返回整个集合,而是按需一次返回一个元素。这在处理大型集合或无限序列时特别有用,因为它可以节省内存
- 关键接口:迭代器的背后是
IEnumerable<T>和IEnumerator<T>接口。有了这些迭代器后,几乎不用手动实现这些接口了
工作原理
迭代器的魔力来自于两个关键字:yield return和yield break
yield return <expression>:向foreach循环“提供”下一个值,并暂停方法的执行,保留所有局部状态。当foreach循环下一次迭代时,该方法会从暂停的地方继续执行yield break:终止迭代,表示序列结束
编译器会将这些包含yield语句的方法或属性转换成一个实现了IEnumerable<T>和IEnumerator<T>的状态机类
创捷迭代器
迭代器方法(返回IEnumerable<T>)
using System;
using System.Collections.Generic;
public class Program
{
public static void Main()
{
// 使用foreach 遍历迭代器方法
foreach (int number in GetEvenNumbers(10))
{
Console.WriteLine(number);
}
// 输出:0 2 4 6 8 10
}
// 迭代器方法,返回IEnumerable<int>
public static IEnumerable<int> GetEvenNumbers(int max)
{
for (int i = 0; i <= max; i++)
{
// 只有当 i 是偶数时,才 yield return
if (i % 2 == 0)
{
// 在这里暂停,将 i 返回给调用者
yield return i;
}
// 当 foreach 循环请求下一个元素时,从这里继续
}
// 方法结束相当于有一个隐式的 yield break
}
}
迭代器Get访问器(返回IEnumerable<T>)
可以为类的属性创建迭代器
using System.Collections.Generic;
public class Zoo
{
private List<string> _animals = new List<string> { "Lion", "Tiger", "Panda", "Elephant" };
// 迭代器属性
public IEnumerable<string> Animals
{
get
{
foreach (var animal in _animals)
{
yield return animal;
}
}
}
}
// 使用
Zoo zoo = new Zoo();
foreach (var animal in zoo.Animals)
{
Console.WriteLine(animal);
}
迭代器执行流程
public static void Main()
{
Console.WriteLine("准备调用 GetNumbers");
IEnumerable<int> numbers = GetNumbers(); // 1. 调用方法,但方法内的代码尚未执行!
Console.WriteLine("准备开始 foreach 循环");
foreach (var num in numbers) // 2. 进入循环时,才第一次执行 GetNumbers 方法体
{
Console.WriteLine($"在循环中获取了: {num}");
}
Console.WriteLine("循环结束");
}
public static IEnumerable<int> GetNumbers()
{
Console.WriteLine("GetNumbers: 开始执行");
yield return 1; // 第一次 foreach 迭代停在这里
Console.WriteLine("GetNumbers: 返回 1 之后");
yield return 2; // 第二次 foreach 迭代停在这里
Console.WriteLine("GetNumbers: 返回 2 之后");
yield return 3; // 第三次 foreach 迭代停在这里
Console.WriteLine("GetNumbers: 返回 3 之后,方法结束");
}
输出结果
准备调用 GetNumbers
准备开始 foreach 循环
GetNumbers: 开始执行
在循环中获取了: 1
GetNumbers: 返回 1 之后
在循环中获取了: 2
GetNumbers: 返回 2 之后
在循环中获取了: 3
GetNumbers: 返回 3 之后,方法结束
循环结束
GetNumbers方法中的代码是和foreach循环交替执行的
底层机制
当编写包含yield return的方法时,C#编译器会进行彻底的代码重写,生成一个实现了IEnumerable<T>和IEnumerator<T>的类
public static IEnumerable<int> GetNumbers()
{
Console.WriteLine("开始");
yield return 1;
Console.WriteLine("在 1 之后");
yield return 2;
Console.WriteLine("在 2 之后");
yield return 3;
Console.WriteLine("结束");
}
编译器生成的近似代码
// 编译器生成的类
[CompilerGenerated]
private sealed class <GetNumbers>d__0 : IEnumerable<int>, IEnumerator<int>
{
// 状态机状态
private int <>1__state;
private int <>2__current;
// 方法参数和局部变量
public int <>3__max;
private int <i>5__1;
// IEnumerator 实现
int IEnumerator<int>.Current => <>2__current;
object IEnumerator.Current => <>2__current;
public <GetNumbers>d__0(int <>1__state)
{
this.<>1__state = <>1__state;
}
private bool MoveNext()
{
switch (<>1__state)
{
case 0:
<>1__state = -1;
Console.WriteLine("开始");
<>2__current = 1; // yield return 1
<>1__state = 1;
return true;
case 1:
<>1__state = -1;
Console.WriteLine("在 1 之后");
<>2__current = 2; // yield return 2
<>1__state = 2;
return true;
case 2:
<>1__state = -1;
Console.WriteLine("在 2 之后");
<>2__current = 3; // yield return 3
<>1__state = 3;
return true;
case 3:
<>1__state = -1;
Console.WriteLine("结束");
return false;
default:
return false;
}
}
// IEnumerable 实现
IEnumerator<int> IEnumerable<int>.GetEnumerator()
{
if (<>1__state == -2) // 第一次调用
{
<>1__state = 0;
return this;
}
return new <GetNumbers>d__0(0);
}
}
- 状态字段(
<>1__state)-2:初始/已释放状态-1:执行中或已完成0, 1, 2, ...:对应每个yield return的位置
- 当前值字段(
<>2__current) 存储当前要返回的元素值 - 局部变量字段
所有局部变量都会被提升为类的字段,以便在
MoveNext()调用之间保持状态
执行流程
跟踪一个foreach循环的执行
foreach (var num in GetNumbers())
{
Console.WriteLine($"获取:{num}");
}
步骤分解:
- 第一次调用
GetEnumerator()
// 编译器生成
IEnumerator<int> enumerator = new <GetNumbers>d__0(0);
// state = 0, 表示从方法开头开始
- 第一次
MoveNext()- 进入
case 0 - 执行
Console.WriteLine("开始") - 设置
<>2__current = 1 - 设置
state = 1(下一个状态) - 返回
true
- 进入
- 第一次读取
Currentforeach读取enumerator.Current得到1- 执行循环体:
Console.WriteLine("获取:1")
- 第二次
MoveNext()- 进入
case 1 - 执行
Console.WriteLine("在 1 之后") - 设置
<>2__current = 2 - 设置
state = 2 - 返回
true
- 进入
- 依此类推…直到
MoveNext()返回false
带局部变量的复杂例子
public static IEnumerable<string> GetMessages(int count)
{
string prefix = "Message_";
for (int i = 0; i < count; i++)
{
yield return prefix + i;
}
Console.WriteLine("完成");
}
编译器转换后的关键部分
[CompilerGenerated]
private sealed class <GetMessages>d__1 : IEnumerable<string>, IEnumerator<string>
{
private int <>1__state;
private string <>2__current;
// 参数和局部变量变为字段
public int count;
public int <>3__count;
private string <prefix>5__1; // 原局部变量 prefix
private int <i>5__2; // 原局部变量 i
private bool MoveNext()
{
switch (<>1__state)
{
case 0:
<>1__state = -1;
<prefix>5__1 = "Message_"; // 初始化局部变量
<i>5__2 = 0; // 循环初始化
goto case 1;
case 1:
if (<i>5__2 < count)
{
<>2__current = <prefix>5__1 + <i>5__2; // yield return
<>1__state = 1;
<i>5__2++; // 循环计数器递增
return true;
}
<>1__state = -1;
Console.WriteLine("完成");
return false;
default:
return false;
}
}
}
重要的设计考虑
为什么需要状态机
- 保持执行上下文:局部变量在
yield之间保持其值 - 恢复执行点:知道下一次从哪里继续执行
- 异常安全:正确处理
try-finally块
try-finally的处理
public IEnumerable<int> WithFinally()
{
try
{
yield return 1;
yield return 2;
}
finally
{
Console.WriteLine("清理");
}
}
编译器会确保finally块在以下情况执行
- 枚举完成时
- 调用
Dispose()时(如foreach提前退出) - 使用
using语句时
性能考虑
优点:
- 延迟执行:只在需要时计算值
- 内存高效:一次只保持一个元素在内存中
缺点:
- 对象分配:每次调用迭代器方法都会创建新的状态机实例
- 虚调用:通过接口调用
MoveNext和Current
调试考虑
虽然源代码看起来是线性的,但调试时要注意
- 不能在某些
yield return语句上设置断点 - 单步执行时会看到在
MoveNext()中跳转 - 局部变量在监视窗口中的表现可能不符合直觉
迭代器的优点
- 简洁性:无需手动实现
IEnumerator的Current,MoveNext(),Reset()成员 - 惰性求值(Lazy Evaluation):只在需要时才计算下一个值,性能高效
- 状态自动管理:编译器生成的状态机自动处理局部变量和执行状态,无需关心循环到了哪一步
- 无限序列:可以表示无限的数学序列
无限序列示例(斐波那契数列)
public static IEnumerable<long> Fibonacci()
{
long a = 0;
long b = 1;
while (true) // 无限循环!
{
yield return a;
long temp = a;
a = b;
b = temp + b;
}
}
// 使用:只取前10个,否则会无限循环下去
foreach (var fib in Fibonacci().Take(10))
{
Console.WriteLine(fib);
}
注意事项
- 不允许在try-catch块中使用
yield return
// 错误写法
public IEnumerable<int> BadExample()
{
try
{
yield return 1; // 编译错误
}
catch
{
// ...
}
}
但是,可以在try块中包装可能抛出异常的代码,只要yield return不在其中
// 正确写法:将yield return 放在 try 块外部
public IEnumerable<int> GoodExample()
{
int result;
try
{
result = SomeOperationThatMightFail();
}
catch
{
result = -1;
}
yield return result;
}
可以使用try-finally(finally块在迭代提前终止时也会执行)
- 返回类型必须是
IEnumerable,IEnumerable<T>,IEnumerator,IEnumerator<T> - 不能包含
ref或in参数