一、Enumerator 接口
实现了 IEnumerator 接口的枚举器包含3个函数成员:Current、MoveNext 以及 Reset。
1)Current: 返回序列中当前位置项的属性。
- 只读属性。
- 返回 Object 类型。可以返回对应的可枚举类型。
2)MoveNext: 枚举器位置移到集合中下一个元素的方法,返回值为 bool,返回值代表位置的状态,是有效位置还是到末尾项的位置。
- 返回值为 true,说明新的位置是有效的。
- 返回值为 false,说明新的位置无效,也有可能到了末尾的位置。
- 枚举器的原始位置在序列中的第一项之前,因此 MoveNext 必须在第一次使用 Current 之前调用。
3)Reset: 把位置重置为原始状态的方法。
二、IEnumberable 接口
可枚举类型之所以能通过 GetEnumerator 方法来获取枚举器对象,是因为 IEnumberable 接口里有这个方法,并且可枚举类型实现了 IEnumberable 接口的类。
class MyClass:IEnumerable
{
public IEnumberator GetEnumerator{...}
}
使用 IEnumerbalbe 和 IEnumerator 的示例
//枚举器
class ColorEnumberator : IEnumerator
{
string[] colors;
int position = -1;
public ColorEnumberator(string[] theColors)
{
colors = new string[theColors.Length];
for (int i = 0; i < theColors.Length; i++)
colors[i] = theColors[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()
{
//创建自定义枚举器ColorEnumberator 对象
return new ColorEnumberator(Colors);//此处断点调试
}
}
class Program
{
static void Main(string[] args)
{
Spectrum spectrum = new Spectrum();
foreach (string color in spectrum)//此处断点调试,可进入到GetEnumerator() 方法里
Console.WriteLine(color);
Console.ReadKey();
}
}
IEnumerable: 用来实现可枚举类型对象,并调用 GetEnumerator 方法获取枚举器
IEnumerator: 用来自定一个枚举器类型。
三、泛型枚举接口
泛型枚举接口:
IEnumerable<T>
IEnumerator<T>
对于非泛型接口:
- IEnumerable 接口的 GetEnumerator 方法返回实现 IEnumerator 的枚举器实例;
- 实现 IEnumerator 的类实现了 Current 属性,它返回 object 类型的引用,然后我们必须把它转化为对象的实际类型。
泛型接口继承自非泛型接口。对于泛型接口形式:
- IEnumerable <T> 接口的GetEnumerator 方法返回实现 IEnumertor <T> 接口的枚举器的实例。
- 实现 IEnumertor <T> 的类实现了 Current 属性,它返回实际类型的实例,而不是object 基类的引用。
- 这些是协变接口,所以它们实际声明为 IEnumerable < out T> 、 IEnumertor < out T>
类型安全
==非泛型接口的实现不是类型安全的。==它们返回 object 类型的引用,然后必须转化为实际类型。
泛型接口的枚举器是类型安全的,它返回实际类型的引用。
图19-7实现 IEnumerator 接口的类的结构
图19-7实现 IEnumerable 接口的类的结构
四、迭代器
编译器为我们创建了枚举器和可枚举类型,这种结构叫作迭代器。
1、声明迭代器
以下是声明两个迭代器的版本
版本1
//声明迭代器 BlackAndWhite;
public IEumerator<string> BlackAndWhite()
{
yield return "black";//如果没有返回,则执行下一条返回语句
yield return "gray";
yield return "white";
}
版本2
//声明迭代器 BlackAndWhite;
public IEumerator<string> BlackAndWhite()
{
string[] theColors = { "black","gray","white" };
for(int i = 0;i < theColors.Length;i++)
yield return theColors[i];
//如果没有返回则再遍历一次;如果能返回,则结束后续迭代
}
yield return 在枚举器中的作用: 每遍历一次访问 Current 属性时,就要返回一个新值,而不是一次返回所有元素。
2、迭代器块
迭代器块是有一个或多个 yield 语句的代码块。
迭代器块有3种:
- 方法主体;
- 访问器主体;
- 运算器主体。
迭代器块与其他代码块不同。其他块包含的语句被当作是命令式的。也就是说,先执行代码块的第一个语句,然后执行后面的语句,最后控制离开块。
另一方面,迭代器不是需要再同一时间执行的一串命令式命令,而是声明性的,它描述了希望编译器为我们创建的枚举器类的行为。迭代器块中的代码描述了如何枚举元素。
迭代器块有两个特殊语句。
- yield return 语句指定了序列中要返回的下一项。
- yield break 语句指定再序列中没有其他项。
可以让迭代器产生枚举器或可枚举类型
//产生枚举器的迭代器
public IEnumerator<string> IteratorMethod()
{
yield retrun...
}
//产生可枚举类型的迭代器
public IEnumerable<string> IteratorMethod()
{
yield retrun...
}
使用迭代器来创建枚举器
class MyClass
{
public int CurrentIndex = 0;
//由于实现了GetEnumerator方法,MyClass 类是可枚举类型
public IEnumerator<string> GetEnumerator()
{
return BlackAndWhite();//返回枚举器
}
public IEnumerator<string> BlackAndWhite()//迭代器
{
yield return "black ";
yield return "gray ";
yield return "white " ;
//返回枚举器 IEnumerator<string>
}
}
class Program
{
static void Main(string[] args)
{
MyClass mc = new MyClass();
foreach (string shade in mc)
Console.WriteLine(shade);
Console.ReadKey();
}
}
输出结果:
black
gray
white
问题: BlackAndWhite 的作用是通过迭代器来创建一个枚举器,可 foreach 语句为什么会遍历输出 BlackAndWhite 方法里的所有返回值呢?
要想回答这个问题,以下我就通过把 BlackAndWhite 方法调整为:
(在 MyClass 类添加了一个 变量 CurrentIndex,以追踪枚举器获取当前值的位置)
public IEnumerator<string> BlackAndWhite()//迭代器
{
//第一次遍历,返回含有black内容的枚举器
yield return "black " + CurrentIndex;
CurrentIndex++;
//第二次遍历,返回含有gray内容的枚举器
yield return "gray " + CurrentIndex;
CurrentIndex++;
//第三次遍历,返回含有white内容的枚举器
yield return "white " + CurrentIndex;
//该注释掉的代码块,其输出结果跟以上不含有CurrentIndex的代码相同
//迭代器里含有字符串数组,是可枚举类型中的内容
//string[] theColors = { "black", "gray", "white" };
//for (int i = 0; i < theColors.Length; i++)
// yield return theColors[i];
}
输出结果:
black 0
gray 1
white 2
由以上的代码可知,foreach 每次遍历访问元素,首先都要调用一次 BlackAndWhite 方法,然后再返回其枚举器,执行 当前 yield return 之后,不会继续执行下行代码,直到下一次遍历的时候,才会继续下一条 yield return 语句。
使用迭代器来创建可枚举类型
class MyClass
{
public IEnumerator<string> GetEnumerator()
{
IEnumerable<string> mEnumerable = BlackAndWhite();//获取可枚举类型
return mEnumerable.GetEnumerator();//获取枚举器
}
//IEnumerable<string>:返回可枚举类型
public IEnumerable<string> BlackAndWhite()
{
yield return "black";
yield return "gray";
yield return "white";
}
}
class Program
{
static void Main(string[] args)
{
MyClass mc = new MyClass();
foreach (string shade in mc)//使用类对象
Console.Write($"{ shade } ");
foreach (string shade in mc.BlackAndWhite())//使用枚举器方法
Console.Write($"{ shade } ");
Console.ReadKey();
}
}
输出结果:
black gray white black gray white
3、总结常见迭代器模式
枚举器的迭代器模式
class MyClass
{
public IEnumerator<string> GetEnumerator()
{
return IteratorMethod();
}
public IEnumerator<string> IteratorMethod()
{
...
yield return ...;
}
}
void Main()
{
MyClass mc = new MyClass();
foreach(string x in mc)
...
}
可枚举类型的迭代器模式
class MyClass
{
public Ienumerator<string> GetEnumerator()
{
return IteratorMethod().GetEnumerator();
}
public IEnumerable<string> IteratorMethod()
{
yield return...
}
void Main()
{
MyClass mc = new MyClass();
foreach(var string x in mc)
...
foreach(string x in mc.IteratorMethod())
...
}
}
4、产生多个可枚举类型
使用迭代器来产生具有两个可枚举类型的类。
Specturm 类有两个可枚举类型的迭代器 —— 一个从紫外线到红外线枚举光谱中的颜色,而另一个以逆序进行枚举。Spectrum 类不是可枚举类型,但是它有两个方法返回可枚举类型。
class Spectrum
{
string[] colors = { "violet", "blue", "cyan", "green", "yellow", "orange", "red" };
//返回可枚举类型
public IEnumerable<string> UVtoIR()
{
for (int i = 0; i < colors.Length; i++)
yield return colors[i];
}
//返回可枚举类型
public IEnumerable<string> IRtoUV()
{
for (int i = colors.Length - 1; i >= 0; i--)
yield return colors[i];
}
}
class Program
{
static void Main(string[] args)
{
Spectrum spectrum = new Spectrum();
foreach (string color in spectrum.UVtoIR())
Console.Write($"{ color } ");
Console.WriteLine();
foreach (string color in spectrum.IRtoUV())
Console.Write($"{ color } ");
Console.WriteLine();
Console.ReadKey();
}
}
输出结果:
violet blue cyan green yellow orange red
red orange yellow green cyan blue violet
5、将迭代器作为属性
本例演示两个方面的内容:
- 1)使用迭代器来产生具有两个枚举器的累;
- 2)演示迭代器如何实现为属性而不是方法。
class Spectrum
{
bool _listFromUVtoIR;
string[] colors = { "violet", "blue", "cyan", "green", "yellow", "orange", "red" };
public Spectrum(bool lisatFromUVtoIR)
{
_listFromUVtoIR = lisatFromUVtoIR;
}
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 (int i = colors.Length - 1; i >= 0; i--)
yield return colors[i];
}
}
}
class Program
{
static void Main(string[] args)
{
Spectrum startUV = new Spectrum(true);
Spectrum startIR = new Spectrum(false);
foreach (string color in startUV)
Console.Write($"{ color } ");
Console.WriteLine();
foreach (string color in startIR)
Console.Write($"{ color } ");
Console.WriteLine();
Console.ReadKey();
}
}
输出结果:
violet blue cyan green yellow orange red
red orange yellow green cyan blue violet
6、迭代器的实质
迭代器的其他重要事项
- 迭代器属于 System.Collection.Generic 命名空间里的需要使用 using 指令引入它。
- 在编译器生成的枚举器中,不支持 Reset 方法。它是接口需要的方法,所以实现了它,但调用时总是抛出 System.NotSupportedException 异常。
在后台,由编译器生成的枚举器类是包含4个状态的状态机。
- Before: 首次调用 MoveNext 之前的初始化状态。
- Running: 调用 MoveNext 后进入这个状态。在这个状态中,枚举器检测并设置下一项的位置。在遇到 yield return、yield break 或在迭代器体结束时,退出状态。
- Suspended: 状态机等待下次调用 MoveNext 的状态。
- After: 没有更多项可以枚举的状态。
如果状态机在 Before 或 Suspended 状态时调用了 MoveNext 方法,就转到了 Running 状态。在Running 状态中,它检测集合的下一项并设置位置。
如果有更多项,状态机会转入 Suspended 状态;如果没有更多项,它转入并保持在 After 状态。