IEnumerable接口允许使用foreach循环。在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>;
利用这个信息就足以建立一个非常简单的示例,如下所示:
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())
WriteLine(item);
ReadKey();
}
为此,静态方法SimpleList()就是迭代器块。它是一个方法,所以使用IEnumerable返回类型。SimpleList()使用yield关键字为使用它的foreach块提供了3个值,每个值都输出到屏幕上。
迭代器返回的是object类型的值,因为object是所有类型的基类,所以可从yield语句返回任何类型。但编译器的智能化程度很高,所以我们可以把返回值解释为foreach循环需要的任何类型。这里代码需要string类型的值,而这正是我们要使用的值。如果修改一行yield代码,让它返回一个整数,就会在foreach循环中出现一个类型转换异常。
对于迭代器,可以使用下面的语句中断将信息返回给foreach循环的过程:
yield break;
在遇到迭代器中的这个语句时,迭代器的处理会立即中断,使用该迭代器的foreach循环也一样。
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)
min = 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 (remainderAfterDivision == 0)
{
isPrime = false;
break;
}
}
if (isPrime)
{
yield return possiblePrime;
}
}
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Console;
namespace Ch11Ex03
{
class Program
{
static void Main(string[] args)
{
Primes primesFrom2To1000 = new Primes(2, 1000);
foreach (long i in primesFrom2To1000)
Write($"{i} ");
ReadKey();
}
}
}
这个示例中的类可以枚举上下限之间的素数集合。封装素数的类利用迭代器提供了这个功能。
Primes的代码开始时比较简单,用两个字段来存储表示搜索范围的最大值和最小值,并使用构造函数设置这些值。注意,最小值是有限制的,它不能小于2,这很合理,因为2是最小的素数。相关的代码则全部放在方法GetEnumerator()中。该方法的签名满足迭代器块的规则,因为它返回的是IEnumerator类型。
为提取上下限之间的素数,需要一次测试每个值,所以用一个for循环开始。由于我们不知道某个数是不是素数,因此先假定这个数是个素数,再看看它是否不是素数。为此,需要看看该数能否被2到概述平方根之间的所有数整除。如果能,则该数不是素数,于是测试下一个数。如果该数的确是素数,就用yield把它传给foreach循环。
这段代码有一个有趣之处:如果把上下限设置为非常大的数,在执行应用程序时,就会发现,会一次显示一个结果,中间有暂停,而不是一次显示所有的结果。这说明,无论代码在yield调用之前是否会终止,迭代器代码都会一次返回一个结果。在后台,调用yield都会中断代码的执行,当请求另一个值时,也就是当使用迭代器的foreach循环开始一个新循环时,代码会恢复执行。
前面曾提到,将介绍迭代器如何用于迭代存储在字典类型的集合中的对象,而不必处理DictionaryItem对象。下面是集合类Animals:
public class Animals : DictionaryBase
{
public void Add(string newID, Animal newAnimal) => Dictionary.Add(newID, newAnimal);
public void Remove(string animalID) => Dictionary.Remove(animalID);
public Animal this[string animalID]
{
get{ return (animal)Dictionary[animalID];}
set{ Dictionary[animalID] = value;}
}
}
可以在这段代码中添加如下的简单迭代器,以便执行预期的操作:
public new IEnumerator GetEnumerator()
{
foreach(object animal in Dictionary.Values)
yiled return (Animal)animal;
}
现在可以使用下面的代码来迭代集合中的Animal对象:
foreach (Animal myAnimal in animalCollection)
{
WriteLine($"New {myAnimal.ToString()} object added to " + $" custom collection, Name = {myAnimal.Name}");
}