迭代器模式

前言

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.为遍历不同的聚合结构提供一个统一的接口

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值