IEnumerable和IEnumerator
为了开始对实现既有.NET接口的研究,让我们先看一下IEnumerable和IEnumerator的作用。C#支持关键字foreach,允许我们遍历任何数组类型的内容。虽然看上去只有数组类型才可以使用这个结构,其实任何支持GetEnumerator()方法的类型都可以通过foreach结构进行运算。
支持这种行为的类或结构实际上是在宣告它们向调用者(如foreach关键字本身)公开了所包含的子项,下面是标准的.NET接口定义:
namespace System.Collections { // 这个接口告知调用方对象的子项可以枚举 public interface IEnumerable{IEnumerator GetEnumerator();} }
可以看到,GetEnumerator()方法返回一个对另一个接口System.Collections.IEnumerator的引用。这个接口提供了基础设施,调用方可以用来移动IEnumerable兼容容器包含的内部对象:
namespace System.Collections {// 这个接口允许调用方获取一个容器的子项 public interface IEnumerator{object Current { get; } // 获取当前的项(只读属性)bool MoveNext(); // 将光标的内部位置向前移动void Reset(); // 将光标重置到第一个成员前} }
如果想自定义类型支持这些接口,可以手工实现每个方法,这需要花费不少精力。虽然自己开发GetNumerator()、MoveNext()、Current()和Reset()也没问题,但是有一个更简单的方法。因为System.Array类型和其他许多类型已经实现了IEnumerable和IEnumerator接口,你可以简单地将请求委托到System.Array,如下所示:
namespace CustomEnumerator {public class Garage : IEnumerable{ // System.Array已经实现了IEnumeratorprivate Car[] carArray = new Car[4]; public Garage(){carArray[0] = new Car("Rusty", 30);carArray[1] = new Car("Clunker", 55);carArray[2] = new Car("Zippy", 30);carArray[3] = new Car("Fred", 30);}public IEnumerator GetEnumerator(){// 返回数组对象的IEnumeratorreturn carArray.GetEnumerator();}} }
修改Garage类型之后,就可以在C# foreach结构中安全使用该类型了。除此之外,GetNumerator()被定义为公开的,对象用户可以与IEnumerator类型交互:
namespace CustomEnumerator {public class Program{static void Main( string[] args ){Console.WriteLine("***** Fun with IEnumerable / IEnumerator *****\n");Garage carLot = new Garage(); foreach (Car c in carLot){Console.WriteLine("{0} is going {1} MPH",c.PetName, c.CurrentSpeed);}// 手动与IEnumerator协作IEnumerator i = carLot.GetEnumerator();i.MoveNext();Car myCar = (Car)i.Current;Console.WriteLine("{0} is going {1} MPH", myCar.PetName, myCar.CurrentSpeed);Console.ReadLine();}} }
如果希望在对象级别隐藏IEnumerable的功能,只需要使用显示接口实现就行了:
IEnumerator IEnumerable.GetEnumerator() {return carArray.GetEnumerator(); }
这样的话,对象用户就不能找到Garage的GetEnumerator()方法,而foreach结构会在必要的时候在背后获得接口。
用yield关键字构建迭代器方法
在以前,如果我们希望构建支持foreach枚举的自定义集合(如Garage),只能实现IEnumerable接口(可能还有IEnumerator接口)。然后,还可以通过迭代器来构建使用foreach循环的类型。
简单来说,迭代器就是这样一个成员方法,他指定了容器内部项被foreach处理时该如何返回。虽然迭代器方法还必须命名为GetEnumerator(),返回值还是必须为IEnumerator类型,但自定义类型不需要实现原来那些接口了。
现在,对当前的Garage类型做如下改进:
public class Garage{private Car[] carArray = new Car[4];...// 迭代器方法public IEnumerator GetEnumerator(){foreach (Car c in carArray){yield return c;}}}
注意,这个GetEnumerator()的实现使用内部foreach逻辑迭代每个子项,使用新的yield return语法向调用方返回每个Car对象。yield关键字用来向调用方的foreach结构指定返回值。当到达yield return语句后,当前位置被存储下来,下次调用迭代器时会从这个位置开始执行。
迭代器方法不一定要通过foreach关键字来返回内容。我们也可以使用如下代码定义迭代器方法:
public IEnumerator GetEnumerator() {yield return carArray[0];yield return carArray[1];yield return carArray[2];yield return carArray[3]; }
在这个实现中,注意GetEnumerator()方法显示返回新的值给调用者。虽然对于这个示例来说意义不是很大,因为如果我们为carArray成员变量增加更多对象的话,GetEnumerator()方法就不会同步。但是,如果我们希望方法返回能被foreach语法处理的局部数据,这个语法就很有用。
构建命名迭代器
还有有趣的一点是,yield关键字从技术上说可以结合任何方法一起使用,无论方法名是什么。这些方法(技术上称为命名迭代器)独特之处在于可以接受许多参数。如果构建命名迭代器的话,需要知道这些方法会返回IEnumerable接口,而不是预计的IEnumerator兼容类型。例如,我们可以为Garage类型增加如下方法:
public IEnumerable GetTheCars( bool ReturnRevesed ) {// 逆序返回项if (ReturnRevesed){for (int i = carArray.Length; i != 0; i--){yield return carArray[i - 1];}}else{// 按顺序返回数组中的项foreach (Car c in carArray){yield return c;}} }
注意,我们的新方法允许调用者以正序和逆序(如果传入的参数值为true)来获取子项。我们可以按如下所示的代码和新方法进行交互:
class Program {static void Main( string[] args ){Console.WriteLine("***** Fun with the Yield Keyword *****\n");Garage carLot = new Garage();// 使用GetEnumerator()来获取项foreach (Car c in carLot){Console.WriteLine("{0} is going {1} MPH",c.PetName, c.CurrentSpeed);}Console.WriteLine();// 使用命名迭代器来获取项(逆序)foreach (Car c in carLot.GetTheCars(true)){Console.WriteLine("{0} is going {1} MPH",c.PetName, c.CurrentSpeed);}Console.ReadLine();} }
命名迭代器是很有用的结构,因为一个自定义容器可以定义多重方式来请求返回的集。
那么,总结一下可枚举对象的构建吧。记住,如果自定义类型要和C#的foreach关键字一起使用的话,容器就需要定义一个名为GetEnumerator()的方法,它由IEnumerator接口类型来定制。通常,这个方法的实现只是交给保存子对象的内部成员,然而,我们也可以使用yield return语法来提供多个"命名迭代器"方法。