当前位置:编程学习 > C#/ASP.NET >>

详解C# 迭代器

迭代器模式是设计模式中行为模式(behavioral pattern)的一个例子,他是一种简化对象间通讯的模式,也是一种非常容易理解和使用的模式。简单来说,迭代器模式使得你能够获取到序列中的所有元素而不用关心是其类型是array,list,linked list或者是其他什么序列结构。这一点使得能够非常高效的构建数据处理通道(data pipeline)--即数据能够进入处理通道,进行一系列的变换,或者过滤,然后得到结果。事实上,这正是LINQ的核心模式。
    在.NET中,迭代器模式被IEnumerator和IEnumerable及其对应的泛型接口所封装。如果一个类实现了IEnumerable接口,那么就能够被迭代;调用GetEnumerator方法将返回IEnumerator接口的实现,它就是迭代器本身。迭代器类似数据库中的游标,他是数据序列中的一个位置记录。迭代器只能向前移动,同一数据序列中可以有多个迭代器同时对数据进行操作。
    在C#1中已经内建了对迭代器的支持,那就是foreach语句。使得能够进行比for循环语句更直接和简单的对集合的迭代,编译器会将foreach编译来调用GetEnumerator和MoveNext方法以及Current属性,如果对象实现了IDisposable接口,在迭代完成之后会释放迭代器。但是在C#1中,实现一个迭代器是相对来说有点繁琐的操作。C#2使得这一工作变得大为简单,节省了实现迭代器的不少工作。
接下来,我们来看如何实现一个迭代器以及C#2对于迭代器实现的简化,然后再列举几个迭代器在现实生活中的例子。
1. C#1:手动实现迭代器的繁琐
    假设我们需要实现一个基于环形缓冲的新的集合类型。我们将实现IEnumerable接口,使得用户能够很容易的利用该集合中的所有元素。我们的忽略其他细节,将注意力仅仅集中在如何实现迭代器上。集合将值存储在数组中,集合能够设置迭代的起始点,例如,假设集合有5个元素,你能够将起始点设为2,那么迭代输出为2,3,4,0,最后是1.
    为了能够简单展示,我们提供了一个设置值和起始点的构造函数。使得我们能够以下面这种方式遍历集合:
object[] values = {"a", "b", "c", "d", "e"};
IterationSample collection = new IterationSample(values, 3);
foreach (object x in collection)
{
    Console.WriteLine (x);
}
 
由于我们将起始点设置为3,所以集合输出的结果是d,e,a,b及c,现在,我们来看如何实现 IterationSample 类的迭代器:
class IterationSample:IEnumerable
{
    Object[] values;
    Int32 startingPoint;
    public IterationSample(Object[] values, Int32 startingPoint)
    {
        this.values = values;
        this.startingPoint = startingPoint;
    }
    public IEnumerator GetEnumerator()
    {
        throw new NotImplementedException();
    }
}
    我们还没有实现GetEnumerator方法,但是如何写GetEnumerator部分的逻辑呢,第一就是要将游标的当前状态存在某一个地方。一方面是迭代器模式并不是一次返回所有的数据,而是客户端一次只请求一个数据。这就意味着我们要记录客户当前请求到了集合中的那一个记录。C#2编译器对于迭代器的状态保存为我们做了很多工作。
       现在来看看,要保存哪些状态以及状态存在哪个地方,设想我们试图将状态保存在IterationSample集合中,使得它实现IEnumerator和IEnumerable方法。咋一看,看起来可能,毕竟数据在正确的地方,包括起始位置。我们的GetEnumerator方法仅仅返回this。但是这种方法有一个很重要的问题,如果GetEnumerator方法调用多次,那么多个独立的迭代器就会返回。例如,我们可以使用两个嵌套的foreach语句,来获取所有可能的值对。这两个迭代需要彼此独立。这意味着我们需要每次调用GetEnumerator时返回的两个迭代器对象必须保持独立。我们仍旧可以直接在IterationSample类中通过相应函数实现。但是我们的类拥有了多个职责,这位背了单一职责原则。
     因此,我们来创建另外一个类来实现迭代器本身。我们使用C#中的内部类来实现这一逻辑。代码如下:
class IterationSampleEnumerator : IEnumerator
{
    IterationSample parent;//迭代的对象  #1
    Int32 position;//当前游标的位置 #2
    internal IterationSampleEnumerator(IterationSample 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 object 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];
        }
    }

    public void Reset()
    {
        position=-1;//将游标重置为-1  #7
    }
}
 
    要实现一个简单的迭代器需要手动写这么多的代码:需要记录迭代的原始集合#1,记录当前游标位置#2,返回元素时,根据当前游标和数组定义的起始位置设置定迭代器在数组中的位置#6。初始化时,将当前位置设定在第一个元素之前#3,当第一次调用迭代器时首先需要调用MoveNext,然后再调用Current属性。在游标自增时对当前位置进行条件判断#4,使得即使当第一次调用MoveNext时没有可返回的元素也不至于出错#5。重置迭代器时,我们将当前游标的位置还原到第一个元素之前#7。
    除了结合当前游标位置和自定义的起始位置返回正确的值这点容易出错外,上面的代码非常直观。现在,只需要在IterationSample类的GetEnumerator方法中返回我们当才编写的迭代类即可:
public IEnumerator GetEnumerator()
{
    return new IterationSampleEnumerator(this);
}
 
    值得注意的是,上面只是一个相对简单的例子,没有太多的状态需要跟踪,不用检查集合在迭代的过程中是否发生了变化。为了实现一个简单的迭代器,在C#1中我们实现了如此多的代码。在使用Framework自带的实现了IEnumerable接口的集合时我们使用foreach很方便,但是当我们书写自己的集合来实现迭代时需要编写这么多的代码。
    在C#1中,大概需要40行代码来实现一个简单的迭代器,现在看看C#2对这一过程的改进。
2. C#2:通过yield语句简化迭代
2.1 引入迭代块(iterator)和yield return 语句
    C#2使得迭代变得更加简单--减少了很多代码量也使得代码更加的优雅。下面的代码展示了再C#2中实现GetEnumerator方法的完整代码:
public IEnumerator GetEnumerator()
{
   for (int index = 0; index < this.values.Length; index++)
    {
        yield return values[(index + startingPoint) % values.Length];
    }
}
    简单几行代码就能够完全实现IterationSampleIterator类所需要的功能。方法看起来很普通,除了使用了yield return。这

补充:软件开发 , C# ,
CopyRight © 2012 站长网 编程知识问答 www.zzzyk.com All Rights Reserved
部份技术文章来自网络,