迭代器是一种设计模式,可以让开发人员无需关心容器对象的底层架构,就可以遍访这个容器对象。简单来说,迭代器就是用来遍历一个序列中的所有对象。
在C#中可以使用foreach关键字就可以枚举一个序列
- foreach (var item in collection)
- {
- Console.WriteLine(item?.ToString());
- }
但foreach语句并非完美无缺,它依赖于.NET Core库中的两个接口:IEnumerable和IEnumerator,接下来我们通过这两个接口实现foreach语句
IEnumerable是可枚举的意思,IEnumerator是枚举器的意思
- public interface IEnumerable
- {
- IEnumerator GetEnumerator();
- }
继承这个接口需要实现暴露出来的GetEnumerator方法,返回一个IEnumerator对象
- public interface IEnumerator
- {
- object Current { get; }
- bool MoveNext();
- void Reset();
- }
IEnumerator接口有三个东西,Current返回当前序列的元素,方法MoveNext()移动到下一个元素,Reset方法重置,所以继承这个接口需要实现这三个东西
从这个两个接口对比就可以发现,对于枚举一个容器,起真正作用是IEnumerator
所以一个对象只要实现IEnumerator接口就能遍历
下面来看一个实例
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using System.Threading.Tasks;
-
- namespace Csharp_study.Day1
- {
- //遍历对象
- public class Anim
- {
- public string name;//动物的名称
- //构造方法,给name赋值
- public Anim(string name)
- {
- this.name = name;
- }
- }
- //枚举器
- public class MIEnumerator : IEnumerator
- {
- Anim[] anim;
- int idx = -1;
- //构造函数,anim赋值
- public MIEnumerator(Anim[] t)
- {
- anim = t;
- }
- //实现IEnumerator接口的Current方法,获取当前元素
- public object Current
- {
- get
- {
- if (idx == -1)
- return new IndexOutOfRangeException();
- return anim[idx];
- }
- }
- //实现IEnumerator接口的MoveNext方法,向下一个元素移动
- public bool MoveNext()
- {
- idx++;
- return anim.Length > idx;
- }
- //实现IEnumerator接口的Reset方法,重置迭代器状态
- public void Reset()
- {
- idx = -1;
- }
- }
- class Class1
- {
- static void Main(string[] args)
- {
- //初始化一个Anim序列,用来遍历
- Anim[] anims = new Anim[] { new Anim("老虎"), new Anim("大象"), new Anim("河马") };
- MIEnumerator enumerator = new MIEnumerator(anims);
- while (enumerator.MoveNext())
- {
- Anim test = enumerator.Current as Anim;
- Console.WriteLine(test.name);
- }
- Console.ReadLine();
- }
-
- }
- }
输出结果
- 老虎
- 大象
- 河马
这段代码就不细说了,看注释就能明白。从这个例子中就可以看出来,我们通过继承这个IEnumerator接口,然后实现它的Current,MoveNext和Reset方法就可以遍历这个Anim对象了。
所以不难看出,foreach关键字就是主要依靠IEnumerator接口实现。那IEnumerable接口是干嘛的呢?上面已经说了,它只有一个GetEnumerator方法,并且返回的是一个IEnumerator类型的对象,所以说IEnumerable的作用就是获得可用于循环访问集合的IEnumerator对象。
我们最终是要通过这两个接口来实现foreach,所以我们对上面的代码进行一下改动
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using System.Threading.Tasks;
-
- namespace Csharp_study.Day1
- {
- //IEnumerable接口
- public class Anim : IEnumerable
- {
- public string name;//动物名称
- Anim[] t;//anim对象数组
- //构造方法重载1-name赋值
- public Anim(string name)
- {
- this.name = name;
- }
- //构造方法重载2-t赋值
- public Anim(Anim[] p)
- {
- t = p;
- }
- //实现IEnumerable接口的GetEnumerator方法
- public IEnumerator GetEnumerator()
- {
- Console.WriteLine("调用GetEnumerator方法!");
- return new MIEnumerator(t);
- }
- }
- //实现IEnumerator接口
- public class MIEnumerator : IEnumerator
- {
- Anim[] anim;
- int idx = -1;
- public MIEnumerator(Anim[] t)
- {
- anim = t;
- }
- //实现IEnumerator接口的Current方法,获取当前元素
- public object Current
- {
- get
- {
- Console.WriteLine("调用Current方法!");
- if (idx == -1)
- return new IndexOutOfRangeException();
- return anim[idx];
- }
- }
- //实现IEnumerator接口的MoveNext方法,向下一个元素移动
- public bool MoveNext()
- {
- Console.WriteLine("调用MoveNext方法!");
- idx++;
- return anim.Length > idx;
- }
- //实现IEnumerator接口的Reset方法,重置迭代器状态
- public void Reset()
- {
- Console.WriteLine("调用Reset方法!");
- idx = -1;
- }
- }
- class Class1
- {
- static void Main(string[] args)
- {
- //初始化一个Anim序列,用来遍历
- Anim[] anims = new Anim[] { new Anim("老虎"), new Anim("大象"), new Anim("河马") };
- Anim anim_t = new Anim(anims);
- //foreach遍历
- foreach (Anim item in anim_t)
- {
- Console.WriteLine(item.name);
- }
- Console.ReadLine();
- }
-
- }
- }
输出结果
- 调用GetEnumerator方法!
- 调用MoveNext方法!
- 调用Current方法!
- 老虎
- 调用MoveNext方法!
- 调用Current方法!
- 大象
- 调用MoveNext方法!
- 调用Current方法!
- 河马
- 调用MoveNext方法!
通过输出结果可以发现,foreach在运行时会先调用Anim类中的GetEnumerator方法获得一个MIEnumerator,然后通过MIEnumerator的MoveNext方法后移,在调用Current方法获得元素的值,之后一直循环MoveNext和Current方法来遍历整个Anim序列
不难得出,GetEnumerator方法负责获取枚举器IEnumerator,MoveNext方法负责向下一个元素移动,Current方法负责返回当前的元素的值,Reset方法负责重置枚举器状态
IEnumerable接口的作用就是获得枚举器IEnumerator,而IEnumerator接口的作用才是迭代的主体,负责遍历序列
通过这两段代码就可以看出foreach遍历的原理是先通过IEnumerable获得枚举器IEnumerator,然后通过枚举器的MoveNext和Current方法来遍历序列。
此外在迭代器中还有一个关键字需要我们掌握-yield。yield是迭代器中的关键字,用于向枚举对象提供值或发出迭代结束的信号
yield是一个语法糖,是为了简化迭代器的实现语法才产生的,从上面的讲解不难发现,在枚举器中起实际起作用的就是MoveNext和Current方法。所以C#2提供一个处理方法:yield语句。
有了yield语句,我们可以在自己的类或者序列中支持foreach迭代而不必实现IEnumerator和IEnumerable接口,我们只要提供一个迭代器,当编辑器检测到迭代器时,会自动生成IEnumerator接口中的Current,MoveNext和Reset方法。
通过yield return xxx 可以向枚举对象提供值,通过yield break来中止迭代
下面可以看一个例子
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using System.Threading.Tasks;
-
- namespace Csharp_study.Day1
- {
- class Class1
- {
- static void Main(string[] args)
- {
- foreach(int item in GetSingleDigitNumbers())
- {
- Console.WriteLine(item);
- }
- IEnumerable<int> GetSingleDigitNumbers()
- {
- yield return 0;
- yield return 1;
- yield return 2;
- yield return 3;
- yield return 4;
- yield return 5;
- yield return 6;
- yield return 7;
- yield return 8;
- yield return 9;
- }
- Console.ReadKey();
- }
- }
- }
输出结果
- 0
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
我们使用IEnumerable
注意,yield只能用于迭代器器,也就是说yield只能用于返回值是IEnumertor和IEnumerable接口及其泛型版本
如有错漏之处,敬请指正!