前言
C#中数据结构都实现了迭代器的功能,具体是如何实现的,接下来就由此篇文章去介绍一下C#是如何实现迭代器的,从使用C#自身提供的接口再到从0到1创建出迭代器。
1.使用C#接口实现迭代器
随便找一个C#实现的数据结构,比如看看列表是如何实现的迭代器功能的,其实网上资料说要实现迭代器,C#已经给出了2个接口,一个是IEumerator还有一个是IEnumerable。来看看List有没有继承这些接口,首先Ctrl+Click进入List实现的代码中,查看一下List到底如何定义的,具体情况如下:
列表确实继承并实现了接口方法,接下来自定义一个迭代器去实现这些接口,看看这些接口到底拟定了那些方法,这些方法的功能又是什么,接下来将一一说明,首先实现这些接口,具体代码如下:
public class Enumerable<T> : IEnumerable<T>
{
public T[] values;
public Int32 startingPoint;
public Enumerable(T[] values, Int32 startingPoint)
{
this.values = values;
this.startingPoint = startingPoint;
}
public IEnumerator GetEnumerator()
{
return new Enumerator<T>(this);
}
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return new Enumerator<T>(this);
}
}
public class Enumerator<T> : IEnumerator<T>
{
Enumerable<T> parent;//迭代的对象 #1
Int32 position;//当前游标的位置 #2
internal Enumerator(Enumerable<T> parent)
{
this.parent = parent;
position = -1;// 数组元素下标从0开始,初始时默认当前游标设置为 -1,即在第一个元素之前, #3
}
public bool MoveNext()
{
if (position != parent.values.Length) //判断当前位置是否为最后一个,如果不是游标自增 #4
{
position++;
}
return position < parent.values.Length;
}
public T Current
{
get
{
if (position == -1 || position == parent.values.Length)//第一个之前和最后一个自后的访问非法 #5
{
throw new InvalidOperationException();
}
Int32 index = position + parent.startingPoint;//考虑自定义开始位置的情况 #6
index = index % parent.values.Length;
return parent.values[index];
}
}
object IEnumerator.Current => throw new NotImplementedException();
public void Reset()
{
position = -1;//将游标重置为-1 #7
}
public void Dispose()
{
throw new NotImplementedException();
}
}
这样就实现了迭代器,这两个接口到底实现了那些方法呢?下面将给出表格:
GetEnumerator | 获取迭代器本身 |
MoveNext | 迭代器下移,返回是否可以继续迭代的状态 |
Current | 获取迭代器当前的值 |
Reset | 重置迭代器 |
接下来尝试使用这个自定义的迭代器,因为实现了.Net自带的IEnumerable接口的集合,可以直接使用foreach就很方便,不然只能使用for循环去调用,具体代码如下:
string[] values = { "a", "b", "c", "d", "e" };
Enumerable<string> collection = new Enumerable<string>(values, 3);
foreach (string x in collection)
{
Console.WriteLine(x);
}
可能会开始思考,去实现IEnumerator接口好像没有必要,因为大部分迭代器去继承这个接口实现逻辑基本差不多的,我们又不需要实现特殊的逻辑的话,难道框架没有提供一个默认迭代器吗?哈哈哈哈,告诉大家还真的提供了一个默认的方式,把IEnumerator去掉,修改GetEnumerator的实现即可,具体代码段如下:
public IEnumerator GetEnumerator()
{
for (int index = 0; index < this.values.Length; index++)
{
yield return values[(index + startingPoint) % values.Length];
}
}
yield return和普通的return可是不一样,它返回了默认的迭代块,实际上当编译器遇到yield return返回的迭代块时,它创建了一个已经实现的内部默认迭代器类。这个类记住了聚合对象的当前位置以及本地变量,包括参数等等,这个默认类实际上类似与我们上面继承IEnumerator接口实现出的类。
2.不用.Net接口会怎么?
其实没有任何问题,咱们尝试一下即可,首先定义一下接口,然后完全靠自己去实现迭代器会怎么样,具体代码如下:
public interface IMEnumerator<T>
{
bool MoveNext();
T Current { get; }
void Reset();
}
public interface IMEnumerable<T>
{
IMEnumerator<T> GetEnumerator();
}
然后继承上面定义的接口,具体实现这些接口的方式是一样的,唯一不同的是调用代码时不能使用foreach,你都不用.Net框架提供的接口了,还想要foreach语法结构清晰的调用迭代器,醒一下醒一下,直接使用for语言调用迭代器算了,具体代码如下:
string[] values = { "a", "b", "c", "d", "e" };
Enumerable<string> collection = new Enumerable<string>(values, 3);
for(Enumerator<string> iter = collection.GetEnumerator(); iter.MoveNext();){
Console.WriteLine(iter.Current);
}
3.接口设计之巧妙
有没有想过,为什么需要设计出2个接口,一个是IEumerator还有一个是IEnumerable,因为IEumerator相当于数据层和获取迭代块的接口,IEnumerable相当于实际迭代块记录状态的接口,这样设计更加的各司其职,代码更好的解耦。迭代器提供了一种方法按照某种顺序访问对象组中各个元素, 而又无须暴露该对象的内部结构的设计方式。如果非要把两个接口合起来实现会有问题吗?答案就是问题不大,咱们可以尝试一下,具体代码如下:
public class Enumerator<T> : IEnumerator<T>
{
public T[] values;
public Int32 startingPoint;
public Enumerator(T[] values, Int32 startingPoint)
{
this.values = values;
this.startingPoint = startingPoint;
}
Int32 position;//当前游标的位置 #2
public bool MoveNext()
{
if (position != values.Length) //判断当前位置是否为最后一个,如果不是游标自增 #4
{
position++;
}
return position < values.Length;
}
public T Current
{
get
{
if (position == -1 || position == values.Length)//第一个之前和最后一个自后的访问非法 #5
{
throw new InvalidOperationException();
}
Int32 index = position + startingPoint;//考虑自定义开始位置的情况 #6
index = index % values.Length;
return values[index];
}
}
object IEnumerator.Current => throw new NotImplementedException();
public void Reset()
{
position = -1;//将游标重置为-1 #7
}
}
这样可以吗?迭代器用完以后,不要把它释放了,重置一下迭代器状态,以上代码也是可以正常迭代获取数据的。但是万一以后这个聚合数据需要换个迭代方式,是直接修改这个类比较好呢,还是单独实现一个新的迭代器,然后修改GetEnumerator获取迭代器的方式比较好呢(万一产品经理又说把迭代器改回来怎么办?可能有人抬杠,把代码回滚一下不就行了(对对对就是你,赶紧滚粗去🙃))。所以数据层和迭代器的本身分开实现比较好,使用迭代器模式可以轻易的换个迭代器,直接如图所示改一个迭代器即可。
总结
1.迭代器优缺点:
优点:1、迭代器支持以不同的方式遍历一个聚合对象,简化了聚合类。 2、在迭代器模式中,增加新的聚合类和迭代器类都很方便,无须修改原有代码。
缺点:由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。
2.迭代器的使用场景:
1.需要为集合对象提供多种遍历方式。
2.为遍历不同的聚合结构提供一个统一的接口