迭代器

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

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

    (1)调用 `collectionObject.GetEnumerator()`,返回一个 `IEnumerator` 引用。这个方法可以通过 `IEnumerable` 接口的实现代码来获得,但这是可选的。

    (2)调用所返回的 `IEnumerator` 接口的 `MoveNext()` 方法。

    (3)如果 `MoveNext()` 方法返回 `true`,就使用 `IEnumerator` 接口的 `Current` 属性来获取对象的一个引用,用于 `foreach` 循环。

    (4)重复前面两步,直到 `MoveNext()` 方法返回 `false` 为止,此时循环停止。

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

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

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

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

    ● 如果要迭代一个类,可使用方法 `GetEnumerator()`,其返回类型是 `IEnumerator`。

    ● 如果要迭代一个类成员,例如一个方法,则使用 `IEnumerable`。

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

    yield return <value>;

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

    public static IEnumerable SimpleList()
    {
        yield return "string 1";
        yield return "string 2";
        yield return "string 3";
    }

    static void Main(string[] args)
    {
        foreach (string item in SimpleList())
            Console.WriteLine(item);

        Console.ReadKey();
    }

  为了亲手测试这些代码,应给 System.Collections 名称空间添加一个 using 语句,或者使用完全限定的 System.Collections.IEnumerable 接口。

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

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

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

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

    yield break;

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

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

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

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

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;

    namespace Ch11Ex03
    {
        public class Primes
        {
            private long min;
            private long max;

            public Primes() : this(2, 100) { }

            public Primes(long minimum, long maximum)
            {
                if (minimum < 2)
                    minimum = 2;
                else
                    min = minimum;
                max = maximum;
            }

            public IEnumerator GetEnumerator()
            {
                for (long possiblePrime = min; possiblePrime <= max; possiblePrime++)
                {
                    bool isPrime = true;
                    for (long possibleFactor = 2; possibleFactor <= 
                    (long)Math.Floor(Math.Sqrt(possiblePrime)); possibleFactor++)
                    {
                        long remainderAfterDivision = possiblePrime % possibleFactor;
                        if (remainerAfterDivision == 0)
                        {
                            isPrime = false;
                            break;
                        }
                    }
                    if (isPrime)
                    {
                        yield return possiblePrime;
                    }
                }
            }
        }
    }

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

    static void Main(string[] args)
    {
        Primes primesFrom2To1000 = new Primes(2, 1000);
        foreach (long i in primesFrom2To1000)
            Console.Write("{0} ", i);
        Console.ReadKey();
    }

  示例的说明

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

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

    public IEnumerator GetEnumerator()
    {
        // 为提取上下限之间的素数,需要依次测试每个值,所以用一个 `for` 循环开始:
        for (long possiblePrime = min; possiblePrime <= max; possiblePrime++)
        {
            // 由于我们不知道某个数是不是素数,所以先假定这个数是素数,再看看它是否不是素数。
            // 为此,需要看看该数能否被 2 到该数平方根之间的所有数整除。
            // 如果能,则该数不是素数,于是测试下一个数。
            // 如果该数的确是素数,就使用 `yield` 把它传送给 `foreach` 循环。

            bool isPrime = true;
            for (long possibleFactor = 2; possibleFactor <= 
            (long)Math.Floor(Math.Sqrt(possiblePrime)); possibleFactor++)
            {
                long remainderAfterDivision = possiblePrime % possibleFactor;
                if (remainerAfterDivision == 0)
                {
                    isPrime = false;
                    break;
                }
            }
            if (isPrime)
            {
                yield return possiblePrime;
            }

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

🔚