本章主要介绍 C# 当中枚举器、可枚举类型以及迭代器相关的知识。
文章目录
- 1. 枚举器和可枚举类型
- 2. IEnumerator 和 IEnumerable 接口
- 2.1 IEnumerator 接口
- 2.2 `IEnumerable` 接口
- 3. 泛型枚举接口
- 4. 迭代器
- 4.1 使用迭代器创建枚举器
- 4.2 使用迭代器创建可枚举类
- 4.3 迭代器作为属性
- 4.4 迭代器的使用场景
- 4.4 迭代器的工作原理
1. 枚举器和可枚举类型
枚举器为我们提供了一种统一的方式来顺序访问集合中的元素,却不必了解集合内部的具体实现。例如,使用 foreach
语句遍历数组中的元素:
int[] arr = {10, 11, 12, 23};
foreach (int item in arr1)
{Console.WriteLine("Item value: {0}", item);
}
可枚举类型允许对象可以被枚举器逐一遍历,也就是说一个类型必须是可枚举的才能使用 foreach
循环进行遍历。
2. IEnumerator 和 IEnumerable 接口
2.1 IEnumerator 接口
一个枚举器必须实现 IEnumerator
接口的 3 个函数成员:
Current
:返回序列中当前位置的属性。这个属性是只读的,返回的是object
类型的引用,所以可以返回任何类型。MoveNext
方法把枚举器位置移动到集合中的下一项。返回值为bool
值,如果新的位置有效,返回true
;否则,返回fasle
。由于枚举器的原始位置在序列中的第一项之前,因此第一次使用Current
之前必须先调用MoveNext
。Reset
把位置重置为原始状态。
设计的思想就是需要一个标志物(具体可以是引用、指针或者其它方式)能够获得被遍历集合的当前项,能够移动到集合中下一项的方法,以及重置当前位置的方法。
举一个例子,下图的左边是自定义的可枚举类型,右边是枚举器。
2.2 IEnumerable
接口
可枚举类是指实现了 IEnumerable
接口的类,该接口只有 GetEnumrator
一个成员方法,它返回用于枚举可枚举类型的枚举器对象。
下面是一个结合使用枚举器和自定义可枚举类的例子:
using System;
using System.Collections;// 自定义整数范围类,实现 IEnumerable 接口
public class IntegerRange : IEnumerable
{private int _start;private int _end;public IntegerRange(int start, int end){_start = start;_end = end;}// 实现 IEnumerable 接口的 GetEnumerator 方法public IEnumerator GetEnumerator(){return new IntegerRangeEnumerator(_start, _end);}// 自定义枚举器类,实现 IEnumerator 接口private class IntegerRangeEnumerator : IEnumerator{private int _start;private int _end;private int _current;private bool _hasStarted;public IntegerRangeEnumerator(int start, int end){_start = start;_end = end;_hasStarted = false;}// 实现 MoveNext(),移动到下一个元素public bool MoveNext(){if (!_hasStarted){_current = _start;_hasStarted = true;}else{_current++;}return _current <= _end;}// 实现 Current 属性,返回当前元素public object Current{get{if (!_hasStarted || _current > _end)throw new InvalidOperationException();return _current;}}// 实现 Reset(),重置枚举器状态public void Reset(){_hasStarted = false;}}
}class Program
{static void Main(){IntegerRange range = new IntegerRange(1, 5);// 使用 foreach 遍历自定义的可枚举类型foreach (int number in range){Console.WriteLine(number);}}
}
输出:
1
2
3
4
5
3. 泛型枚举接口
在 C# 中,泛型枚举接口主要包括两个接口:IEnumerable<T>
和 IEnumerator<T>
。这两个接口分别用于实现泛型集合的枚举功能,允许集合中的元素在类型安全的情况下被逐个遍历。与非泛型版本相比,泛型接口提供了更好的类型安全性和性能,因为它避免了装箱/拆箱操作以及对 object
的转换。
对于泛型接口形式:
IEnumerable<T>
接口的GetEnumerator
方法返回实现IEnumerator
枚举器类的实例;- 实现
IEnumerator<T>
的类实现了 Current 属性,它返回实际类型的对象,而不是object
基类的引用。
下面是一个实现 IEnumerable<T>
和 IEnumerator<T>
的简单示例,定义了一个泛型范围类 Range<T>
,可以生成从某个起始值到终止值的泛型集合。
using System;
using System.Collections;
using System.Collections.Generic;// 定义一个泛型范围类,实现 IEnumerable<T>
public class Range<T> : IEnumerable<T> where T : IComparable<T>
{private T _start;private T _end;private Func<T, T> _incrementFunc;public Range(T start, T end, Func<T, T> incrementFunc){_start = start;_end = end;_incrementFunc = incrementFunc;}// 实现 IEnumerable<T> 接口public IEnumerator<T> GetEnumerator(){return new RangeEnumerator(_start, _end, _incrementFunc);}// 显式实现非泛型的 IEnumerable 接口IEnumerator IEnumerable.GetEnumerator(){return GetEnumerator();}// 自定义泛型枚举器private class RangeEnumerator : IEnumerator<T>{private T _current;private T _start;private T _end;private Func<T, T> _incrementFunc;private bool _hasStarted;public RangeEnumerator(T start, T end, Func<T, T> incrementFunc){_start = start;_end = end;_incrementFunc = incrementFunc;_hasStarted = false;}// 实现 IEnumerator<T> 的 Current 属性public T Current{get{if (!_hasStarted)throw new InvalidOperationException();return _current;}}// 实现非泛型的 Current 属性object IEnumerator.Current => Current;// 实现 MoveNext(),移动到下一个元素public bool MoveNext(){if (!_hasStarted){_current = _start;_hasStarted = true;}else{_current = _incrementFunc(_current);}return _current.CompareTo(_end) <= 0;}// 实现 Reset(),重置枚举器public void Reset(){_hasStarted = false;}// 实现 Dispose() 方法public void Dispose() { }}
}class Program
{static void Main(){// 定义一个整数范围的泛型集合var range = new Range<int>(1, 5, x => x + 1);// 使用 foreach 遍历集合foreach (var number in range){Console.WriteLine(number);}}
}
Range<T>
代码解释:
Range<T>
是一个泛型类,允许生成从_start
到_end
范围的集合。它实现了IEnumerable<T>
接口,使该集合可以被 foreach 遍历。incrementFunc
是一个Func<T, T>
类型的委托,指定如何生成下一个值。这使得Range<T>
可以用于不同类型的泛型数据。
输出:
1
2
3
4
5
4. 迭代器
C# 从 2.0 版本开始提供了创建枚举器和可枚举类型的简单方式——迭代器。迭代器可为我们自动创建枚举器和可枚举类型。通过迭代器,程序可以按需逐步生成和返回集合中的元素,而无需一次性加载所有元素。
C# 的迭代器是通过使用两个关键字实现的:
yield return
:用于按需返回集合中的下一个元素。yield break
:用于立即终止迭代。
当你实现一个迭代器方法时,C# 编译器会自动生成一个实现 IEnumerable
或 IEnumerable<T>
接口的类,并生成适当的 IEnumerator
或 IEnumerator<T>
枚举器,因此不需要手动编写这些接口的实现代码。迭代器方法可以像常规方法一样被调用,并与 foreach
语句兼容。
4.1 使用迭代器创建枚举器
下面的实例展示了使用迭代器创建枚举器:
class MyClass
{public IEnumerator<string> GetEnumerator(){return BlackAndWhite(); // 返回枚举器}// 直接产生一个枚举器public IEnumerator<string> BlackAndWhite(){yield return "Black";yield return "White";yield return "gray";}class Program{static void Main(string[] args){MyClass myClass = new MyClass();foreach (string color in myClass){Console.WriteLine(color);}}}
}
输出如下:
Black
White
gray
编译器自动帮助我们做的工作如下图所示。编译器会为我们添加一个实现一个枚举器必须包含的方法。
4.2 使用迭代器创建可枚举类
下面的示例展示了使用迭代器创建可枚举类,这里的 BlackAndWhite
方法返回可枚举类型而不是枚举器,因此在 GetEnumrator
方法中需要调用可枚举类型对象的 GetEnumerator
方法来获取枚举器。
class MyClass1
{public IEnumerator<string> GetEnumerator(){IEnumerable<string> myEnum = BlackAndWhite(); // 获取可枚举类型return myEnum.GetEnumerator(); // 获取枚举器}public IEnumerable<string> BlackAndWhite(){yield return "Black";yield return "White";yield return "gray";}}class Program
{static void Main(string[] args){MyClass1 myClass1 = new MyClass1();// 使用类对象foreach (string color in myClass1){Console.WriteLine(color);}// 使用类枚举方法foreach (string color in myClass1.BlackAndWhite()){Console.WriteLine(color);}}
}
4.3 迭代器作为属性
下面的代码示例主要用来演示两个方面的内容:
- 使用迭代器产生具有两个枚举器的类;
- 演示迭代器作为属性而不是方法
这段代码声明了两个属性来定义两个不同的枚举器。GetEnumerator
方法根据_listFromUVtoIR
布尔变量的值返回两个枚举器中的一个。如果_listFromUVtoIR
为true
,则返回UVtoIR
枚举器;否则,返回IRtoUV
枚举器。
class Spectrum
{bool _listFromUVtoIR;string[] colors = { "Red", "Green", "Blue", "Yellow", "Purple", "Orange" };public Spectrum(bool isFromUVtoIR){_listFromUVtoIR = isFromUVtoIR;}public IEnumerator<string> GetEnumerator(){return _listFromUVtoIR? UVtoIR: IRtoUV;}public IEnumerator<string> UVtoIR{get{for (int i = 0; i < colors.Length; i++){yield return colors[i];}}}public IEnumerator<string> IRtoUV{get {for (nint i = colors.Length - 1; i >= 0; i--)yield return colors[i];}}
}class Program
{static void Main(){Spectrum startUV = new Spectrum(true);Spectrum startIR = new Spectrum(false);foreach (string color in startUV){Console.Write("{0} ", color);}Console.WriteLine();foreach (string color in startIR){Console.Write("{0} ", color);}}
}
输出:
Red Green Blue Yellow Purple Orange
Orange Purple Yellow Blue Green Red
下面代码的说明。
public IEnumerator<string> IRtoUV
{get {for (nint i = colors.Length - 1; i >= 0; i--)yield return colors[i];}
}
IRtoUV
是一个只读属性,它的类型是 IEnumerator<string>
。当访问该属性时,get
访问器中的代码会被执行,返回一个 IEnumerator<string>
对象。这个 get 访问器使用了 yield return
,因此它定义了一个迭代器,用于逐步返回 colors
数组中的元素,按从后往前的顺序。
4.4 迭代器的使用场景
迭代器的使用场景如下:
- 延迟执行
迭代器提供了按需生成元素的能力,这意味着元素只有在被请求时才会生成。这对于处理大数据集合、流数据或计算开销较大的数据非常有用。举个例子,如果你有一个需要大量计算才能得到的值,使用迭代器可以避免一次性计算所有值,而是每次只计算一个值。
static IEnumerable<int> GenerateLargeNumbers()
{for (int i = 0; i < int.MaxValue; i++){yield return i;}
}
- 自定义集合的遍历
当你创建自定义的数据结构时,可以使用迭代器来定义集合的遍历规则,而不需要手动实现IEnumerable
和IEnumerator
接口。例如,使用迭代器遍历二叉树:
using System;
using System.Collections.Generic;// 定义二叉树节点类
public class TreeNode
{public int Value; // 节点的值public TreeNode Left; // 左子节点public TreeNode Right; // 右子节点public TreeNode(int value){Value = value;}// 使用迭代器实现深度优先遍历(前序遍历:根 -> 左 -> 右)public IEnumerable<int> PreOrderTraversal(){// 返回当前节点的值yield return Value;// 如果左子节点存在,递归遍历左子树if (Left != null){foreach (var leftValue in Left.DepthFirstTraversal()){yield return leftValue;}}// 如果右子节点存在,递归遍历右子树if (Right != null){foreach (var rightValue in Right.DepthFirstTraversal()){yield return rightValue;}}}// 中序遍历:左 -> 根 -> 右)public IEnumerable<int> InOrderTraversal(){if (Left != null){foreach (var leftValue in Left.InOrderTraversal()){yield return leftValue;}}yield return Value;if (Right != null){foreach (var rightValue in Right.InOrderTraversal()){yield return rightValue;}}}// 后序遍历:右 -> 根 -> 左public IEnumerable<int> PostOrderTraversal()
{if (Left != null){foreach (var leftValue in Left.PostOrderTraversal()){yield return leftValue;}}if (Right != null){foreach (var rightValue in Right.PostOrderTraversal()){yield return rightValue;}}yield return Value;
}}class Program
{static void Main(){// 构建一棵二叉树TreeNode root = new TreeNode(1);root.Left = new TreeNode(2);root.Right = new TreeNode(3);root.Left.Left = new TreeNode(4);root.Left.Right = new TreeNode(5);root.Right.Left = new TreeNode(6);root.Right.Right = new TreeNode(7);// 使用深度优先遍历二叉树Console.WriteLine("DFS Traversal (Pre-order):");foreach (int value in root.PreOrderTraversal()){Console.WriteLine(value);}}
}
输出:
DFS Traversal (Pre-order):
1 2 4 5 3 6 7
DFS Traversal (In-order):
4 2 5 1 6 3 7
DFS Traversal (Post-order):
4 5 2 6 7 3 1
二叉树的形状:
4.4 迭代器的工作原理
在后台,由编译器生成的枚举器类是包含4个状态的状态机。
Before
首次调用 MoveNext 的初始状态。Running
调用MoveNext后进入这个状态。在这个状态中,枚举器检测并设置下一项的位置。在遇到yield return
、yield break
或在迭代器体结束时,退出状态。Suspended
状态机等待下次调用MoveNext
的状态。After
没有更多项可以枚举。
好了,以上就是对 C# 当中枚举器、可枚举类以及迭代器的介绍。如有收获,记得一键三连呐。
最后,给各位道友介绍一下使用国外虚拟卡开通一些国外服务的渠道,当前我在用的是 wildcard,使用我的注册邀请码 IOQ1YDHH
注册,你能得 2 美刀呢。能抵消一部分的开卡费用。自己买个 Github Copilot
、ChatGPT Plus
(不用注册账号即可使用)简直不要太香。