构建可枚举类型(IEnumerable和IEnumerator)
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已经实现了IEnumerator private 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() { // 返回数组对象的IEnumerator return 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语法来提供多个"命名迭代器"方法。