迭代器

  本章前面介绍过,IEnumerable 接口允许使用 foreach 循环。在 foreach 循环中并不是只能使用集合类(如本章前面所示的几个集合类),相反,在 foreach 循环中使用定制类通常有很多优点。

  但是,重写使用 foreach 循环。在 foreach 循环中,迭代一个 collectionObject 集合的过程如下:

  1. 1)调用 `collectionObject.GetEnumerator()`,返回一个 `IEnumerator` 引用。这个方法可以通过 `IEnumerable` 接口的实现代码来获得,但这是可选的。
  2. 2)调用所返回的 `IEnumerator` 接口的 `MoveNext()` 方法。
  3. 3)如果 `MoveNext()` 方法返回 `true`,就使用 `IEnumerator` 接口的 `Current` 属性来获取对象的一个引用,用于 `foreach` 循环。
  4. 4)重复前面两步,直到 `MoveNext()` 方法返回 `false` 为止,此时循环停止。

  所以,为在类中进行这些操作,必须重写几个方法,跟踪索引,维护 Current 属性,以及执行其他一些操作,这要做许多工作。

  一个较简单的替代方法是使用迭代器。使用迭代器将有效地自动生成许多代码,正确地完成所有任务。而且,使用迭代器的语法掌握起来非常容易。

  迭代器的定义是,它是一个代码块,按顺序提供了要在 foreach 循环中使用的所有值。一般情况下,这个代码块是一个方法,但也可以使用属性访问器和其他代码块作为迭代器。这里为简单起见,仅介绍方法。

  无论代码块是什么,其返回类型都是有限制的。与期望正好相反,这个返回类型与所枚举的对象类型不同。例如,在表示 Animal 对象集合的类中,迭代器的返回类型不可能是 Animal。两种可能的返回类型是前面提到的接口类型 IEnumerableIEnumerator。使用这两个类型的场合是:

  1. 如果要迭代一个类,可使用方法 `GetEnumerator()`,其返回类型是 `IEnumerator`
  2. 如果要迭代一个类成员,例如一个方法,则使用 `IEnumerable`

  在迭代器块中,使用 yield 关键字选择要在 foreach 循环中使用的值。其语法如下:

  1. yield return <value>;

  利用这个信息就足以建立一个非常简单的示例,如下所示(包含在代码文件 SimpleIterators\Program.cs 中):

  1. public static IEnumerable SimpleList()
  2. {
  3. yield return "string 1";
  4. yield return "string 2";
  5. yield return "string 3";
  6. }
  7. static void Main(string[] args)
  8. {
  9. foreach (string item in SimpleList())
  10. Console.WriteLine(item);
  11. Console.ReadKey();
  12. }
  为了亲手测试这些代码,应给 System.Collections 名称空间添加一个 using 语句,或者使用完全限定的 System.Collections.IEnumerable 接口。

  在此,静态方法 SimpleList() 就是迭代器块。它是一个方法,所以使用 IEnumerable 返回类型SimpleList() 使用 yield 关键字为使用它的 foreach 块提供了 3 个值,每个值都输出到屏幕上。

  显然,这个迭代器并不是特别有用,但它确实能够演示迭代器的机制,说明实现迭代器有多么简单。看看代码,读者可能会疑惑代码是如何知道返回 string 类型的项。实际上,并没有返回 string 类型的项,而是返回了 object 类型的值。因为 object 是所有类型的基类,所以可从 yield 语句中返回任意类型。

  但编译器的智能程度很高,所以我们可以把返回值解释为 foreach 循环需要的任何类型。这里代码需要 string 类型的值,所以这就是我们要使用的值。如果修改一行 yield 代码,让它返回一个整数,就会在 foreach 循环中出现一个类型转换异常。

  对于迭代器,还有一点要注意。可以使用下面的语句中断将信息返回给 foreach 循环的过程:

  1. yield break;

  在遇到迭代器中的这个语句时,迭代器的处理会立即中断,使用该迭代器的 foreach 循环也一样。

  下面是一个较复杂但很有用的示例。在这个示例中,要实现一个迭代器,获取素数。

  ● (1)在 C:\BegVCSharp\Chapter11 目录中创建一个新控制台应用程序 Ch11Ex03

  ● (2)添加一个新类 Primes,修改 Primes.cs 中的代码,如下所示:

  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using System.Text;
  6. using System.Threading.Tasks;
  7. namespace Ch11Ex03
  8. {
  9. public class Primes
  10. {
  11. private long min;
  12. private long max;
  13. public Primes() : this(2, 100) { }
  14. public Primes(long minimum, long maximum)
  15. {
  16. if (minimum < 2)
  17. minimum = 2;
  18. else
  19. min = minimum;
  20. max = maximum;
  21. }
  22. public IEnumerator GetEnumerator()
  23. {
  24. for (long possiblePrime = min; possiblePrime <= max; possiblePrime++)
  25. {
  26. bool isPrime = true;
  27. for (long possibleFactor = 2; possibleFactor <=
  28. (long)Math.Floor(Math.Sqrt(possiblePrime)); possibleFactor++)
  29. {
  30. long remainderAfterDivision = possiblePrime % possibleFactor;
  31. if (remainerAfterDivision == 0)
  32. {
  33. isPrime = false;
  34. break;
  35. }
  36. }
  37. if (isPrime)
  38. {
  39. yield return possiblePrime;
  40. }
  41. }
  42. }
  43. }
  44. }

  (3)修改 Program.cs 中的代码,如下所示:

  1. static void Main(string[] args)
  2. {
  3. Primes primesFrom2To1000 = new Primes(2, 1000);
  4. foreach (long i in primesFrom2To1000)
  5. Console.Write("{0} ", i);
  6. Console.ReadKey();
  7. }
  示例的说明

  这个示例中的类可以枚举上下限之间的素数集合。封装素数的类利用迭代器提供了这个功能。

  Primes 的代码开始时比较简单,用两个字段存储表示搜索范围的最大值和最小值,并使用构造函数设置这些值。注意,最小值是有限制的,它不能小于 2,这很合理,因为 2 是最小的素数。相关的代码则全部放在方法 GetEnumerator() 中。该方法的签名满足迭代器块的规则,因为它返回 IEnumerator 类型:

  1. public IEnumerator GetEnumerator()
  2. {
  3. // 为提取上下限之间的素数,需要依次测试每个值,所以用一个 `for` 循环开始:
  4. for (long possiblePrime = min; possiblePrime <= max; possiblePrime++)
  5. {
  6. // 由于我们不知道某个数是不是素数,所以先假定这个数是素数,再看看它是否不是素数。
  7. // 为此,需要看看该数能否被 2 到该数平方根之间的所有数整除。
  8. // 如果能,则该数不是素数,于是测试下一个数。
  9. // 如果该数的确是素数,就使用 `yield` 把它传送给 `foreach` 循环。
  10. bool isPrime = true;
  11. for (long possibleFactor = 2; possibleFactor <=
  12. (long)Math.Floor(Math.Sqrt(possiblePrime)); possibleFactor++)
  13. {
  14. long remainderAfterDivision = possiblePrime % possibleFactor;
  15. if (remainerAfterDivision == 0)
  16. {
  17. isPrime = false;
  18. break;
  19. }
  20. }
  21. if (isPrime)
  22. {
  23. yield return possiblePrime;
  24. }

  在这段代码中,有一个有趣之处:如果把上下限设置为非常大的数,在执行应用程序时,就会发现,会一次显示一个结果,中间有暂停,而不是一次显示所有结果。这说明,无论代码在 yield 调用之间是否终止,迭代器代码都会一次返回一个结果。在后台,调用 yield 都会中断代码的执行,当请求另一个值时,也就是当使用迭代器的 foreach 循环开始一个新循环时,代码会恢复执行。