深入理解Csharp迭代器

知道91 | .Net | 2015-12-06 | 阅读:4061

什么是迭代器,其实我们在代码中很多情况下都会接触到它,废话不多说,直入正题。

迭代器模式是设计模式中行为模式(behavioral pattern)的一个例子,他是一种简化对象间通讯的模式,也是一种非常容易理解和使用的模式。简单来说,迭代器模式使得你能够获取到序列中的所有元素 而不用关心是其类型是array,list,linked list或者是其他什么序列结构。这一点使得能够非常高效的构建数据处理通道(data pipeline)--即数据能够进入处理通道,进行一系列的变换,或者过滤,然后得到结果。事实上,这正是LINQ的核心模式。

csharp

在.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
}

除了结合当前游标位置和自定义的起始位置返回正确的值这点容易出错外,上面的代码非常直观。现在,只需要在IterationSample类的GetEnumerator方法中返回我们当才编写的迭代类即可:

public IEnumerator GetEnumerator()
{
    return new IterationSampleEnumerator(this);
}

2. C#2:通过yield语句简化迭代

C#2使得迭代变得更加简单--减少了很多代码量也使得代码更加的优雅。下面的代码展示了再C#2中实现GetEnumerator方法的完整代码:

public IEnumerator GetEnumerator()
{
    for (int index = 0; index < this.values.Length; index++)
    {
        yield return values[(index + startingPoint) % values.Length];
    }
}

2.2 迭代器的执行流程

如下的代码,展示了迭代器的执行流程,代码输出(0,1,2,-1)然后终止。

class Program {

  static readonly String Padding = new String(' ', 30);
  static IEnumerable CreateEnumerable()
  {
      Console.WriteLine("{0} CreateEnumerable()方法开始", Padding);
      for (int i = 0; i < 3; i++)
      {
          Console.WriteLine("{0}开始 yield {1}", i);
          yield return i;
          Console.WriteLine("{0}yield 结束", Padding);
      }
      Console.WriteLine("{0} Yielding最后一个值", Padding);
      yield return -1;
      Console.WriteLine("{0} CreateEnumerable()方法结束", Padding);
  }

  static void Main(string[] args)
  {
      IEnumerable iterable = CreateEnumerable();
      IEnumerator iterator = iterable.GetEnumerator();
      Console.WriteLine("开始迭代");
      while (true)
      {
          Console.WriteLine("调用MoveNext方法……");
          Boolean result = iterator.MoveNext();
          Console.WriteLine("MoveNext方法返回的{0}", result);
          if (!result)
          {
              break;
          }
          Console.WriteLine("获取当前值……");
          Console.WriteLine("获取到的当前值为{0}", iterator.Current);
      }
      Console.ReadKey();
  }
}

从输出结果中可以看出一下几点:

  • 直到第一次调用MoveNext,CreateEnumerable中的方法才被调用。
  • 在调用MoveNext的时候,已经做好了所有操作,返回Current属性并没有执行任何代码。
  • 代码在yield return之后就停止执行,等待下一次调用MoveNext方法的时候继续执行。
  • 在方法中可以有多个yield return语句。
  • 在最后一个yield return执行完成后,代码并没有终止。调用MoveNext返回false使得方法结束。
  • 第一点尤为重要:这意味着,不能在迭代块中写任何在方法调用时需要立即执行的代码--比如说参数验证。如果将参数验证放在迭代块中,那么他将不能够很好的起作用,这是经常会导致的错误的地方,而且这种错误不容易发现。下面来看如何停止迭代,以及finally语句块的特殊执行方式。

微软

2.3 迭代器的特殊执行流程

在普通的方法中,return语句通常有两种作用,一是返回调用者执行的结果。二是终止方法的执行,在终止之前执行finally语句中的方法。在上面的例子中,我们看到了yield return语句只是短暂的退出了方法,在MoveNext再次调用的时候继续执行。在这里我们没有写finally语句块。如何真正的退出方法,退出方法时finnally语句块如何执行,下面来看看一个比较简单的结构:yield break语句块。 使用 yield break 结束一个迭代

static IEnumerable CountWithTimeLimit(DateTime limit)
{
    try
    {
        for (int i = 1; i <= 100; i++)
        {
            if (DateTime.Now >= limit)
            {
                yield break;
            }
            yield return i;
        }
    }
    finally
    {
        Console.WriteLine("停止迭代!"); Console.ReadKey();
    }
}
static void Main(string[] args)
{
    DateTime stop = DateTime.Now.AddSeconds(2);
    foreach (Int32 i in CountWithTimeLimit(stop))
    {
        Console.WriteLine("返回 {0}", i);
        Thread.Sleep(300);
    }
}