目录
3. 使用 IEnumerable 和 IEnumerator 案例
一、可枚举类型和枚举器
数组为什么可以被foreach语句处理?原因是数组按需提供了一个枚举器对象。
- foreach结构设计用来和可枚举类型一起使用。
- 可枚举类型可以通过GetEnumerator方法获取对象的枚举器。
- 从枚举器中请求每一项并且把它作为迭代变量,该变量是只读的。
1. 枚举器
枚举器需要实现IEnumerator接口,其中包含三个函数成员:Current、MoveNext、Reset。
因为数组是可枚举类型,所以我们可以通过GetEnumerator方法获取其枚举器对象,来模拟foreach遍历,通过模拟我们可以初步认识枚举器接口中的这三个方法。
internal class Program
{
static void Main(string[] args)
{
int[] ints = { 5, 6, 7 };
IEnumerator enumerator = ints.GetEnumerator();//获取数组的枚举器对象
while (enumerator.MoveNext()) //MoveNext方法移动指向数组中下标的指针位置,并判断是否在数组长度范围内,返回一个bool值
{
int current = (int)enumerator.Current;//获取当前指针指向的元素
Console.WriteLine(current);
}
bool beforeReset = enumerator.MoveNext();
Console.WriteLine("位置重置为原始位置之前,enumerator.MoveNext()方法返回:{0}", beforeReset);
enumerator.Reset();//将位置重置为原始状态
//当位置没有重置时,enumerator.MoveNext()返回fasle,所以不会进入这个while循环,无法遍历
while (enumerator.MoveNext())
{
int current = (int)enumerator.Current;
Console.WriteLine(current);
}
}
}
2. 可枚举类
前面说过数组是可枚举类型,其原因在于数组实现了IEnumerable接口。可枚举类就是指实现了IEnumerable接口的类。
综上,可枚举类型是实现了IEnumerable接口的类,实现了IEnumerable接口中的GetEnumerator方法返回一个枚举器,枚举器是实现了 IEnumerator接口的类,通过实现IEnumerator接口三个方法来访问元素。理解图如下:
3. 使用 IEnumerable 和 IEnumerator 案例
//枚举器
class ColorEnumerator : IEnumerator
{
string[] _colors;
int _position = -1;
public ColorEnumerator(string[] colors)
{
_colors = new string[colors.Length];
Array.Copy(colors,_colors,colors.Length);
}
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 = { "赤", "橙", "黄", "绿", "青", "蓝", "紫" };
public IEnumerator GetEnumerator()
{
return new ColorEnumerator(colors);
}
}
//测试
internal class Program
{
static void Main(string[] args)
{
Spectrum colors = new Spectrum();
foreach (string color in colors)
Console.WriteLine(color);
}
}
4. 泛型枚举接口
- 泛型枚举接口的使用和非泛型枚举接口的使用是差不多的
- 非泛型枚举接口的实现不是类型安全的,它们返回object类型的引用,必须强转位实际类型
- 泛型枚举接口的实现是类型安全的,它们返回的是实际类型的对象,而非object基类的引用
- 应该尽量使用泛型枚举接口
二、迭代器
- 迭代器可以代替我们手动编码的可枚举类和枚举器。
- 迭代器块是有一个或多个yield语句的代码块。
- 迭代器块描述了希望编译器为我们创建的枚举器类的行为。
- 迭代器块可以是方法主体、访问器主体或运算符主体。
- 迭代器块中有两个特殊语句:
i. yield return语句指定了序列中返回的下一项
ii.yield break语句指定在序列中没有其他项
1. 使用迭代器创建枚举器
代码示例:
class MyClass
{
public IEnumerator<string> GetEnumerator() //返回枚举器
{
return BlackAndWhite();
}
public IEnumerator<string> BlackAndWhite() //迭代器块
{
yield return "赤";
yield return "橙";
yield return "黄";
yield return "绿";
yield return "青";
yield return "蓝";
yield return "紫";
}
}
//测试
internal class MyTest
{
static void Main(string[] args)
{
MyClass myClass = new MyClass();
foreach (string element in myClass)
Console.WriteLine(element);
}
}
代码图解:
2. 使用迭代器创建可枚举类
代码示例:
class MyClass
{
public IEnumerator<string> GetEnumerator() //返回枚举器
{
return BlackAndWhite().GetEnumerator();//返回从可枚举类获取的枚举器
}
public IEnumerable<string> BlackAndWhite() //返回可枚举类
{
yield return "赤";
yield return "橙";
yield return "黄";
yield return "绿";
yield return "青";
yield return "蓝";
yield return "紫";
}
}
//测试
internal class MyTest
{
static void Main(string[] args)
{
MyClass myClass = new MyClass();
foreach (string element in myClass)//让类本身可枚举
Console.WriteLine(element);
Console.WriteLine("--------");
foreach (string element in myClass.BlackAndWhite())//调用返回可枚举类的方法
Console.WriteLine(element);
}
}
代码图解:
3. 常见的迭代器模式
通过前面的代码案例,创建迭代器可以用来产生可枚举类型和枚举器。总结如下:
- 如果我们创建返回枚举器的迭代器时,必须实现GetEnumerator方法来让类可枚举。
- 如果我们创建返回可枚举类型的迭代器时,我们有两种选择:
选择1:实现GetEnumerator让类本身可枚举
选择2:不实现GetEnumerator让类本身不可枚举,但仍可使用由迭代器产生的可枚举类
4. 产生多个枚举类型
可以在同一个类中创建多个迭代器来产生多个枚举类型。
//注意:该类中没有实现GetEnumerator方法,所以该类本身不可以被枚举,但可以通过迭代器返回的枚举类型进行遍历
class MyClass
{
string[] colors = { "赤", "橙", "黄", "绿", "青", "蓝", "紫" };
public IEnumerable<string> PrintOut() //迭代器返回可枚举类型
{
for (int i = 0; i < colors.Length; i++)
yield return colors[i];
}
public IEnumerable<string> ReversePrintOut() //迭代器返回可枚举类型
{
for(int i = colors.Length-1; i >= 0; i--)
yield return colors[i];
}
}
//测试
internal class MyTest
{
static void Main(string[] args)
{
MyClass myClass = new MyClass();
foreach (string color in myClass.PrintOut())
Console.WriteLine(color);
Console.WriteLine("--------");
foreach (string color in myClass.ReversePrintOut())
Console.WriteLine(color);
}
}
5. 将迭代器作为属性
可以将迭代器作为属性。代码示例如下:
class Colors
{
bool chooseEnumerator;
string[] colors = { "赤", "橙", "黄", "绿", "青", "蓝", "紫" };
public Colors(bool b)
{
chooseEnumerator = b;
}
//根据创建类对象时传入的布尔值控制返回不同的枚举器
public IEnumerator<string> GetEnumerator()
{
return chooseEnumerator ? PrintOut : ReversePrintOut;
}
public IEnumerator<string> PrintOut //迭代器放置在属性get访问器中
{
get
{
for (int i = 0; i < colors.Length; i++)
yield return colors[i];
}
}
public IEnumerator<string> ReversePrintOut //迭代器放置在属性get访问器中
{
get
{
for (int i = colors.Length - 1; i >= 0; i--)
yield return colors[i];
}
}
}
//测试
internal class MyTest
{
static void Main(string[] args)
{
Colors colors = new Colors(true);
foreach (string color in colors)
Console.WriteLine(color);
Console.WriteLine("--------");
Colors reColors = new Colors(false);
foreach (string color in reColors)
Console.WriteLine(color);
}
}
6. 迭代器的实质
- 迭代器需要using指令引入System.Collections.Generic的命名空间。
- 在编译器生成的枚举器中,Reset方法没有实现,调用会抛异常。
- 由编译器生成的枚举器是包含四个状态的状态机。
- Before:首次调用MoveNext的初始状态。
- Running:调用MoveNext后进入这个状态。在这个状态中,枚举器检查并设置下一项的位置,在遇到yield return、yield break或在迭代器体结束时,退出状态。
- Suspended:状态机等待下次调用MoveNext的状态。
- After:没有更多项可以枚举。
(注:本章学习总结自《C#图解教程》)