19.1 枚举器和可枚举类型
19.1.1 枚举器的概述
我们可以使用foreach语句遍历数组中的元素,什么数组可以这么做?原因是数组可以按需提供一个叫作枚举器的对象,枚举器可以依次返回请求的数组中的元素。
对于有枚举器的类型而言,必须有一种方法来获取它。获取对象举器的方法是调用对象的GetEnumerator方法,实现该方法的类型叫做可枚举类型。
foreach结构设计用来和可枚举类型一起使用。只要给它的遍历对象是可枚举类型,比如数组,它就会执行如下行为:①通过调用GetEnumerator方法获取对象的枚举器 ②从枚举器中请求每一项并且把它作为迭代变量,代码可以取该变量但不可以改变
19.1.2 IEnumerator接口
实现了 IEnumerator接口的枚举器包含3个函数成员:Current、MoveNext 以及 Reset。
①current是返回序列中当前位置项的属性,它是只读属性,它返回object类型的引用,所以可以返回任何类型的对象。
②MoveNext 是把枚举器位置前进到集合中下一项的方法。它也返回布尔值,指示新的位置是有效位置还是已经越界:如果新的位置是有效的,方法返回true,否则返回false;枚举器的原始位置在序列中的第一项之前,因此MoveNext必须在第一次使用Current之前调用。
③Reset 是把位置重置为原始状态的方法。
枚举器跟踪序列中当前项的方式完全取决于实现。可以通过对象引用、索引值或其他方式来实现。对于内置的一维数组来说,就使用项的索引。
19.1.3 IEnumerable接口
可枚举类是指实现了IEnumerable接口的类。IEnumerable接口只有一个成员GetEnumerator方法,它返回对象的枚举器。
19.1.4 示例
cs
class ColorEnumerator : IEnumertor
{
string[] colors;
int position = -1;
public ColorEnumerator(string[] theColors)
{
colors = new string[theColors.Length];
for(int i = 0; i < theColor.Length; i++)
colors[i] = theColor[i];
}
public object Current
{
get
{
if(position == -1)
throw new InvalidOperationException();
if(position >= colors.Length)
throw new InvalidOperationException();
return colors[position];
}
}
public bool MoveNext()
{
if(position < colors.Length - 1)
{
position++;
return true;
}
else
return false;
}
public void Reset()
{
position = -1;
}
}
class Spectrum : IEnumerable
{
string[] Colors = {"violet", "blue", "cyan", "green", "yellow", "orange", "red"};
public IEnumerator GetEnumerator()
{
return new ColorEnumerator(Colors);
}
}
class Program
{
static void Main()
{
Spectrum spectrum = new Spectrum();
foreach(string color in spectrum)
Console.WriteLine(color);
}
}
19.1.5 泛型枚举接口
实际上我们大多使用泛型版本的IEnumerator<T>和IEnumerable<T>,他们的主要区别如下所示:
对于非泛型接口形式:①lEnumerable接口的 GetEnumerator 方法返回实现IEnumerator 的枚举器类实例 ②实现IEnumerator的类实现了Current属性,它返回object类型的引用,然后我们必须把它转化为对象的实际类型。
泛型接口继承自非泛型接口,对于泛型接口形式:①IEnumerable<T>接口的 GetEnumerator方法返回实现IEnumerator<T>的枚举器类的实例 ②实现IEnumerator<T>的类实现了Current属性,它返回实际类型的实例,而不是object基类的引用 ③这些是协变接口,所以它们的实际声明就是IEnumerable<out T>和IEnumerator<out T>
需要重点注意的是,我们目前所看到的非泛型接口的实现不是类型安全的。它们返回object类型的引用,然后必须转化为实际类型。而泛型接口的枚举器是类型安全的,它返回实际类型的引用。如果要创建自己的可枚举类应该实现这些泛型接口。
19.2 迭代器
19.2.1 概述
在 C# 中,迭代器(Iterator) 是一种简化集合遍历逻辑的机制,它允许开发者自定义序列的生成和遍历方式,而无需手动实现复杂的IEnumerable/IEnumerator接口。迭代器的核心是yield关键字,结合 C# 的状态机编译特性,极大地降低了遍历逻辑的实现成本
迭代器的本质是实现了遍历逻辑的代码块 ,它能够生成一个序列,并支持逐个取出序列中的元素。在 C# 中,迭代器主要用于:①让自定义类型支持foreach循环遍历 ②动态生成序列(如无限序列、分页序列、计算型序列) ③实现 LINQ(Language Integrated Query)的延迟执行特性(LINQ to Objects 的核心就是迭代器)
传统上,若要让一个类支持foreach,需手动实现IEnumerable/IEnumerator接口,管理迭代状态(如当前索引、遍历完成标记等),代码繁琐且易出错。迭代器通过yield关键字将这一过程自动化,开发者只需关注序列的生成逻辑即可。
19.2.2 迭代器的核心接口
| 接口 | 核心成员 | 作用 |
|---|---|---|
IEnumerable |
IEnumerator GetEnumerator() |
表示一个可枚举的集合,提供获取迭代器的方法。 |
IEnumerator |
object Current { get; }、bool MoveNext()、void Reset() |
表示一个迭代器,负责遍历集合的元素,管理迭代状态。 |
IEnumerable<T> |
继承IEnumerable,新增IEnumerator<T> GetEnumerator() |
泛型可枚举接口,避免装箱 / 拆箱,类型安全。 |
IEnumerator<T> |
继承IEnumerator和IDisposable,新增T Current { get; } |
泛型迭代器,支持类型安全的元素访问和资源释放(Dispose)。 |
IDisposable:泛型迭代器实现IDisposable,确保迭代过程中的资源(如using块、数据库连接)被正确释放。
19.2.3 yield关键字
yield关键字是 C# 迭代器的核心,用于定义迭代器块(Iterator Block),分为两种形式:①yield return <表达式>表示将表达式的结果作为序列的下一个元素返回,执行到该语句时,迭代器会暂停执行 ,并保存当前状态;下次调用MoveNext()时,从暂停处继续执行。 ②yield break表示立即终止序列,迭代器的MoveNext()将返回false,后续代码不再执行。
迭代器方法(或属性、索引器)需满足以下条件:
①返回类型 :必须是IEnumerable、IEnumerable<T>、IEnumerator、IEnumerator<T>中的一种(泛型版本优先使用)
②方法体 :包含yield return或yield break(仅包含yield break时生成空序列)
③修饰符 :不能是async(C# 8.0 后支持异步迭代器,使用IAsyncEnumerable<T>)、ref/out参数,也不能是void返回类型。
19.2.4 迭代器的工作原理
C# 编译器会将包含yield的迭代器块转换为一个私有嵌套类 (状态机),该类实现了对应的IEnumerable<T>/IEnumerator<T>接口,并管理迭代的状态。状态机的核心逻辑包括:
①状态字段 :一个int类型的字段(如_state),表示迭代器的当前执行位置(初始值为 - 1,0 表示未开始,1、2... 表示暂停后的位置,-2 表示完成)
②当前元素字段 :存储Current属性的值(如_current)
③MoveNext () 方法 :根据_state的值执行对应的代码块,直到遇到yield return(设置_current,_state更新为当前位置,返回true)或yield break(_state设为 - 2,返回false) ④Dispose () 方法 :清理迭代器的状态(如释放using块中的资源、恢复_state为 - 2)
19.2.5 迭代器的典型应用场景
①自定义集合类的遍历:让自定义集合支持foreach循环,只需实现IEnumerable<T>接口,并通过迭代器方法实现GetEnumerator(),无需手动编写迭代器类。
cs
public class Node<T>
{
public T Value { get; set; }
public Node<T> Next { get; set; }
public Node(T value) => Value = value;
}
public class MyLinkedList<T> : IEnumerable<T>
{
private Node<T> _head;
public void Add(T value)
{
if (_head == null)
{
_head = new Node<T>(value);
return;
}
Node<T> current = _head;
while (current.Next != null) current = current.Next;
current.Next = new Node<T>(value);
}
// 迭代器方法实现GetEnumerator()
public IEnumerator<T> GetEnumerator()
{
Node<T> current = _head;
while (current != null)
{
yield return current.Value; // 逐个返回节点值
current = current.Next;
}
}
// 非泛型版本(显式接口实现)
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
// 调用示例
var list = new MyLinkedList<int>();
list.Add(1);
list.Add(2);
foreach (int num in list) Console.WriteLine(num); // 输出1、2
②生成无限序列:由于迭代器是延迟执行的,可生成无限序列(如斐波那契数列、自然数序列),遍历时分批获取所需元素,避免内存溢出。
cs
public static IEnumerable<long> Fibonacci()
{
long a = 0, b = 1;
while (true) // 无限循环
{
yield return a; // 延迟返回
long temp = a + b;
a = b;
b = temp;
}
}
// 调用:取前10个元素(LINQ的Take()也是延迟执行)
foreach (long num in Fibonacci().Take(10))
{
Console.WriteLine(num); // 输出0、1、1、2、3、5、8、13、21、34
}
③分页数据处理:从数据库、文件或网络中分页读取数据时,迭代器可逐页加载数据,避免一次性加载所有数据到内存,降低内存占用。
cs
// 模拟从数据库分页获取数据
public static List<DataRow> GetDataFromDB(int pageIndex, int pageSize)
{
// 实际开发中使用ADO.NET/EFCore分页查询
var data = new List<DataRow>();
// 模拟数据:若pageIndex>3,返回空列表
if (pageIndex <= 3)
{
for (int i = 0; i < pageSize; i++)
{
data.Add(new DataRow { Id = pageIndex * pageSize + i });
}
}
return data;
}
// 迭代器:逐页读取数据
public static IEnumerable<DataRow> GetPagedData(int pageSize)
{
int pageIndex = 1;
while (true)
{
var pageData = GetDataFromDB(pageIndex, pageSize);
if (pageData.Count == 0) yield break; // 无数据时终止
foreach (var row in pageData) yield return row; // 返回当前页数据
pageIndex++;
}
}
// 调用:遍历所有分页数据
foreach (var row in GetPagedData(2))
{
Console.WriteLine($"数据ID:{row.Id}"); // 输出0、1、2、3、4、5(共3页,每页2条)
}
// 模拟DataRow类
public class DataRow { public int Id { get; set; } }
④序列的组合与转换:迭代器可灵活组合、筛选或转换多个序列,实现类似 LINQ 的Concat、Where、Select等功能。
cs
public static IEnumerable<T> Concat<T>(IEnumerable<T> seq1, IEnumerable<T> seq2)
{
foreach (T item in seq1) yield return item; // 先遍历第一个序列
foreach (T item in seq2) yield return item; // 再遍历第二个序列
}
// 调用
var seq1 = new List<int> { 1, 2 };
var seq2 = new List<int> { 3, 4 };
foreach (int num in Concat(seq1, seq2)) Console.WriteLine(num); // 输出1、2、3、4
⑤LINQ to Objects 的底层实现:LINQ to Objects 的大部分方法(如Where、Select、Take、Skip、GroupBy等)都是通过迭代器实现的,这也是 LINQ 支持延迟执行 的核心原因。例如,Where方法的简化实现
cs
public static IEnumerable<TSource> Where<TSource>(
this IEnumerable<TSource> source,
Func<TSource, bool> predicate)
{
foreach (TSource item in source)
{
if (predicate(item)) yield return item; // 满足条件时返回元素
}
}