迭代器
本章前面介绍过,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
。两种可能的返回类型是前面提到的接口类型 IEnumerable
和 IEnumerator
。使用这两个类型的场合是:
● 如果要迭代一个类,可使用方法 `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
循环开始一个新循环时,代码会恢复执行。