什么是迭代器
迭代器是一种设计模式,可以让开发人员无需关心容器对象的底层架构,就可以遍访这个容器对象。简单来说,迭代器就是用来遍历一个序列中的所有对象。
在C#中可以使用foreach关键字就可以枚举一个序列
foreach (var item in collection)
{
Console.WriteLine(item?.ToString());
}
但foreach语句并非完美无缺,它依赖于.NET Core库中的两个接口:IEnumerable和IEnumerator,接下来我们通过这两个接口实现foreach语句
IEnumerable是可枚举的意思,IEnumerator是枚举器的意思
IEnumerable接口
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
继承这个接口需要实现暴露出来的GetEnumerator方法,返回一个IEnumerator对象
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是迭代器中的关键字,用于向枚举对象提供值或发出迭代结束的信号
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<int>来表示一个int序列,通过yield return 来向这个序列中添加元素,然后直接在foreach就可以遍历这个int序列了,完全不需要MoveNext方法和Current方法就可以实现遍历。通过yield系统会自动生成这两种方法。
注意,yield只能用于迭代器器,也就是说yield只能用于返回值是IEnumertor和IEnumerable接口及其泛型版本
如有错漏之处,敬请指正!