本章主要介绍 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
简直不要太香。