1. 迭代器模式的核心价值
在C#开发中,迭代器模式(Iterator Pattern)是一种基础但极其重要的设计模式。它提供了一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。这种模式在.NET框架中有着广泛的应用,比如foreach循环的背后就是迭代器在发挥作用。
为什么我们需要迭代器?想象一下你有一个包含百万条记录的集合,如果每次都需要将整个集合加载到内存中才能遍历,那将造成巨大的内存压力。而迭代器通过yield return实现了延迟加载(lazy loading),只有在真正需要时才生成下一个元素,这种"按需生产"的特性大大提升了程序效率。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. C#迭代器的实现机制
2.1 yield关键字的工作原理
yield return是C#迭代器的核心语法糖。当编译器遇到yield return语句时,它会自动生成一个实现了IEnumerator接口的状态机类。这个生成的类会记住当前执行的位置,并在下次调用MoveNext()时从该位置继续执行。
csharp复制public IEnumerable<int> GetNumbers()
{
yield return 1;
yield return 2;
yield return 3;
}
上面这段简单的代码,编译器会将其转换为一个包含状态字段的复杂类,这个状态字段记录了当前执行到了哪个yield return语句。每次调用MoveNext()时,状态机就会跳转到对应的位置继续执行。
2.2 迭代器的执行流程
一个完整的迭代器生命周期包含以下几个关键阶段:
- 调用迭代器方法时,实际上并没有立即执行方法体,而是创建并返回一个状态机对象
- 第一次调用MoveNext()时,状态机开始执行方法体,直到遇到第一个yield return
- yield return表达式的值被赋给Current属性
- 控制权返回给调用者,状态机暂停
- 下次调用MoveNext()时,状态机从暂停的位置继续执行
- 当方法执行完毕或遇到yield break时,MoveNext()返回false,迭代结束
3. 高级迭代器技巧
3.1 自定义迭代器实现
虽然yield return很方便,但有时我们需要更精细的控制,这时可以直接实现IEnumerator接口:
csharp复制public class CustomIterator<T> : IEnumerator<T>
{
private readonly T[] _items;
private int _index = -1;
public CustomIterator(T[] items) => _items = items;
public bool MoveNext() => ++_index < _items.Length;
public T Current => _items[_index];
object IEnumerator.Current => Current;
public void Reset() => _index = -1;
public void Dispose() { }
}
这种实现方式虽然代码量更大,但可以完全控制迭代过程,适合需要特殊迭代逻辑的场景。
3.2 迭代器与资源管理
迭代器的一个常见陷阱是资源释放问题。由于迭代器可能在任何yield return处暂停,传统的using语句无法保证资源及时释放。正确的做法是让迭代器实现IDisposable:
csharp复制public IEnumerable<string> ReadLines(string filePath)
{
using var reader = new StreamReader(filePath);
while (!reader.EndOfStream)
{
yield return reader.ReadLine();
}
}
注意这里using语句包裹了整个迭代过程,确保即使迭代未完成,资源也能被正确释放。
4. 迭代器性能优化
4.1 避免装箱操作
当处理值类型集合时,不当的迭代器实现可能导致频繁的装箱拆箱操作:
csharp复制// 不好的实现 - 会导致装箱
public IEnumerable GetNumbers()
{
yield return 1; // 装箱发生在这里
yield return 2;
}
// 好的实现 - 使用泛型避免装箱
public IEnumerable<int> GetNumbers()
{
yield return 1; // 无装箱
yield return 2;
}
4.2 延迟执行的陷阱
迭代器的延迟执行特性虽然节省内存,但有时会导致意外结果:
csharp复制var numbers = GetNumbers(); // 此时方法还未执行
list.Add(4); // 修改了数据源
foreach (var n in numbers) // 迭代时才会实际执行
{
Console.WriteLine(n); // 可能包含后来添加的4
}
如果需要在确定的时间点捕获集合状态,可以调用ToList()或ToArray()立即执行迭代:
csharp复制var numbers = GetNumbers().ToList(); // 立即执行并保存结果
5. 实际应用场景
5.1 分页处理大数据集
迭代器非常适合处理大型数据集的分页加载:
csharp复制public IEnumerable<Customer> GetCustomersInBatches(int batchSize)
{
int skip = 0;
while (true)
{
var batch = db.Customers
.OrderBy(c => c.Id)
.Skip(skip)
.Take(batchSize)
.ToList();
if (batch.Count == 0) yield break;
foreach (var customer in batch)
{
yield return customer;
}
skip += batchSize;
}
}
这种方法可以保持低内存占用,同时处理任意大小的数据集。
5.2 无限序列生成
迭代器可以轻松表示数学上的无限序列:
csharp复制public IEnumerable<int> Fibonacci()
{
int a = 0, b = 1;
while (true)
{
yield return a;
(a, b) = (b, a + b);
}
}
// 使用Take获取有限数量的元素
foreach (var num in Fibonacci().Take(10))
{
Console.WriteLine(num);
}
6. 常见问题与解决方案
6.1 迭代过程中修改集合
在foreach循环中修改集合会导致InvalidOperationException:
csharp复制var list = new List<int> { 1, 2, 3 };
foreach (var item in list)
{
list.Add(item * 2); // 抛出异常
}
解决方案:
- 迭代前创建集合的副本
- 使用for循环代替foreach
- 如果需要修改,可以先收集修改项,迭代完成后再应用
6.2 多次迭代问题
标准迭代器通常不支持多次迭代:
csharp复制var numbers = GetNumbers();
foreach (var n in numbers) { /* 第一次迭代 */ }
foreach (var n in numbers) { /* 可能无法再次迭代 */ }
解决方法:
- 每次需要迭代时调用迭代器方法
- 将结果缓存到列表或数组
- 实现可重置的迭代器
6.3 线程安全问题
迭代器默认不是线程安全的。如果集合可能在迭代过程中被其他线程修改,应该:
- 在迭代期间锁定集合
- 使用并发集合类型
- 创建集合的快照进行迭代
csharp复制lock (syncObject)
{
foreach (var item in collection)
{
// 安全地处理item
}
}
7. 迭代器模式的最佳实践
- 优先使用yield return语法,它生成的代码经过高度优化
- 为值类型集合使用泛型迭代器(IEnumerable
)避免装箱 - 考虑实现IDisposable来正确管理资源
- 注意延迟执行特性可能带来的副作用
- 在需要多次迭代或线程安全时采取适当措施
- 对于性能关键路径,考虑使用自定义迭代器实现
- 合理使用LINQ方法,但要注意它们也会创建迭代器
迭代器是C#中一个强大但常被低估的特性。深入理解其工作原理和最佳实践,可以帮助我们编写出更高效、更健壮的代码。从简单的集合遍历到复杂的流水线处理,迭代器模式都能提供优雅的解决方案。
