C#中的IEnumerable、IEnumerator与foreach深入探讨
1. 了解foreach
C#中的foreach是一个集合遍历语法糖,之所以称foreach为语法糖是因为foreach语句本身不是语言的一部分,它总是会被编译成基本的for循环语句。但一直以来,我们都使用foreach语法来遍历集合,它直观、而又易于理解,同时也减少了我们的代码量。
如下面的代码,它用于输出集合中所有的元素:
namespace LangPrac { class Program { static voidMain(string[] args) { Stack<int> Ints = new Stack<int>(); for (int i =0; i < 10; i++) { Ints.Push(i+ 2); } foreach (int item in Ints) { Console.Write(item+" "); } } } }
变量Ints是一个int型的栈,但如果读者不知道栈是什么,可以将Ints理解为一个集合,集合里的元素都是整数。下面的for循环将10个整数压入栈中,于是Ints这个集合中有10个整数了。接下来利用foreach语句将在栈Ints中的数据全部输出。相信各位读者理解上面的代码并不困难,但作为一位程序员,我们不应该满足于只理解和会使用foreach语句,我们还应该知道foreach是怎么实现的。
上面我们提过,foreach只是一个语法糖,它最终都会被替换为基本的for循环。那么foreach的功能究竟是怎么实现的呢?下面我们来一起探讨。
我们首先定义了一个Book类,然后又定义了一个Book集合类Books。具体代码如下:
class Book { public string ISBN{ get; set; } public int Price {get; set; } } class Books { public Book[]intArray; public Books() { intArray = newBook[10]; for (int i =0; i < intArray.Length; i++) { intArray[i] = new Book(); intArray[i].ISBN = "7923001" + i; intArray[i].Price = 2 * i + 30; } } }
显然Books类并不是一个真正意义上的集合类,但Books里有一个intArray成员,它是一个数组——也是就集合,我们通过这种方法来模拟Books作为一个集合类。
因为Books是一个集合,因此我们希望能通过foreach语句来输出Books里的所有元素,编写下面的语句:
Books books = new Books(); foreach (Book book in books) { Console.WriteLine(book.ISBN+ "++:" + book.Price); }
但很可惜,VisualStudio的智能感知告诉我们有如下错误:
错误 1 “LangPrac.Books”不包含“GetEnumerator”的公共定义,因此 foreach 语句不能作用于“LangPrac.Books”类型的变量。
错误的原因是Books类不包含“GetEnumerator”的公共定义,为了使得foreach语句作用于Books类型的变量,必须要在Books中增加公共的GetEnumerator方法定义,于是,修改的Books类如下:
class Books { public Book[]intArray; public Books() { intArray = newBook[10]; for (int i = 0; i <intArray.Length; i++) { intArray[i] = new Book(); intArray[i].ISBN = "7923001" + i; intArray[i].Price = 2 * i + 30; } } public IiteratorGetEnumerator() { return newIiterator(this); } }
增加的定义为:
public Iiterator GetEnumerator() { return newIiterator(this); }
这里在类中定义了GetEnumerator方法,它的返回类型是Iiterator,我们暂时不关心GetEnumerator的实现,首先来看一下Iiterator类型是什么。
在这里,我不得不首先揭晓谜底——为了使得foreach语句能够作用于自定义的集合类(我们这里的自定义集合类为Books),自定义的集合类必须有一个GetEnumerator公共方法,方法的返回类型是这样的一个类——它至少包含一个公共的Current字段和一个公共的MoveNext方法
Iiterator类型的定义如下:
class Iiterator { private BooksCollectionObject;//集合对象实例 public Book Current { get; set; }//当前集合中正在被访问的元素 private int Index;//指示到达的位置 publicIiterator(Books t) { this.CollectionObject = t; Index = -1; } public boolMoveNext()//将当前元素移动到下一个,并返回已到集合最后的信号。返回false代表已经集合元素已经遍历完毕 { if (Index <CollectionObject.intArray.Length-1) {//如果索引还在集合范围之内 Current =CollectionObject.intArray[++Index];//取得当前元素 returntrue; } returnfalse;//如果返回false,Current就不会再被访问 } }
可以看到,Current表示当前正在被访问的集合类中的元素,而MoveNext方法的作用是将Current移动到集合类中的下一个元素。于是,我们可以猜测,foreach的实现原理是通过对集合类的公共方法GetEnumerator的调用来获取集合的元素的。GetEnumerator的返回类型统称为迭代器,而迭代器可以取得集合类的元素对象,同时将当前的指向移动到下一个元素并做集合中的元素是否全部遍历完成的检查。因为迭代器的行为是自定义的,所以集合的实现方式到底是数组还是链表这点并不重要,迭代器总是能够取得当前元素并将指向移动到下一个元素。(C++的使用者一定会对这点非常亲切)
运行上面的程序
Books books = new Books(); foreach (Book book in books) { Console.WriteLine(book.ISBN+ "++:" + book.Price); }
输出为:
2. 基础类库中的IEnumerable和IEnumerator接口
我们从非泛型的Stack类谈起,为了简单起见,下面只给出一个简单的Stack类定义:
// 摘要: // 表示对象的简单后进先出 (LIFO) 非泛型集合。 public class Stack : ICollection,IEnumerable, ICloneable { public Stack(); public virtual IEnumeratorGetEnumerator(); public virtual object Peek(); public virtual object Pop(); public virtual void Push(object obj); }
可以看到,Stack继承了IEnumerable接口,同也有一个GetEnumerator方法,它返回的是一个IEnumerator(迭代器)。事实上,GetEnumerator方法是从IEnumerable中继承而来的。
IEnumerable定义如下: publicinterface IEnumerable { // 摘要: // 返回一个循环访问集合的枚举数。 // // 返回结果: // 一个可用于循环访问集合的 System.Collections.IEnumerator 对象。 [DispId(-4)] IEnumerator GetEnumerator(); }
基础类库中的所有可以在foreach语句中使用的集合类都实现了IEnumerable接口,而IEnumerable接口只有一个方法GetEnumerator,也就是继承了IEnumerable接口的类必须实现GetEnumerator方法,只有这样才能使得集合类支持foreach语法。注意到GetEnumerator方法返回一个IEnumerator(迭代器),而IEnumerator的定义为:
// 摘要: // 支持对非泛型集合的简单迭代。 [ComVisible(true)] [Guid("496B0ABF-CDEE-11d3-88E8-00902754C43A")] public interface IEnumerator { // 摘要: // 获取集合中的当前元素。 // // 返回结果: // 集合中的当前元素。 object Current { get; } // 摘要: // 将枚举数推进到集合的下一个元素。 // // 返回结果: // 如果枚举数成功地推进到下一个元素,则为 true;如果枚举数越过集合的结尾,则为 false。 // // 异常: // System.InvalidOperationException: // 在创建了枚举数后集合被修改了。 bool MoveNext(); // // 摘要: // 将枚举数设置为其初始位置,该位置位于集合中第一个元素之前。 // // 异常: // System.InvalidOperationException: // 在创建了枚举数后集合被修改了。 void Reset(); }
它有一个公开的Current字段和一个MoveNext方法。同上面讨论的一样,Current用于指向集合里的元素,而MoveNext方法将Current移动到集合的下一个元素。这就是基础类库中集合类支持foreach语句的根本原因。
由上面的分析可知,集合类要支持foreach语句,就必须要实现IEnumerable接口,而IEnumerable接口只有一个方法GetEnumerator,这个方法返回一个IEnumerator(迭代器),从GetEnumerator的返回类型IEnumerator中可以得到集合中的元素,并将指向移动到集合的下一个元素。事实上,我们在上面的讨论中已经用代码实现了接口IEnumerable和接口IEnumerator相似的功能。
说到这里,我们来个不得不说一下foreach语句的行为了,foreach是一个编译时“语法”,使用foreach语句的类必须有一个GetEnumerator方法,这是程序在编译时通过字符串查找的,同理,GetEnumerator方法的返回类型也必须至少有一个公开的Current字段和公开的MoveNext方法,编译器也是通过字符串查找的方式来找到它们的。即使你定义的其他方法和字段可以达到同样的功能,这都是行不通的,以上的名称必须为GetEnumerator、Current和MoveNext。
3. 构建可枚举集合的其他方法
可枚举的集合(使用foreach语句)是一个诱人的特性,为了实现可枚举的集合,还有一些简单的方法。
还记得上面的Books类吗?
<em>class Books { public Book[] intArray; public Books() { intArray = new Book[10]; for (int i = 0; i <intArray.Length; i++) { intArray[i] = new Book(); intArray[i].ISBN ="7923001" + i; intArray[i].Price = 2 * i + 30; } } public Iiterator GetEnumerator() { return new Iiterator(this); } } </em>它实现的GetEnumerator是返回一个自定义的Iiterator(迭代器),其实完全不必要如此大动干戈,因为我们是通过数组的方式来实现这个集合的,所以只需要返回一个数组的迭代器就可以了。请看下面的GetEnumerator方法修改:<em> public IEnumerator GetEnumerator() { return intArray.GetEnumerator(); }</em>
GetEnumerator()的返回类型改为IEnumerator,而GetEnumerator方法是由数组本身实现的,而我们要遍历的集合类也是由数组实现的。如果你不明白,那就想一想一个这样的事实:所有的数组都支持foreach语句遍历访问,同样也包括自定义类型的数组。
构建迭代器方法(GetEnumerator)的另一方式是使用yield关键字。修改Books里的GetEnumerator方法,代码如下:
publicIEnumerator GetEnumerator() { foreach (Book item in intArray) { yield return item; } }
yield关键字用来向调用方法的foreach结构指定返回值。当到达yield语句后,当前位置被存储下来,下次调用迭代器时会从这个位置开始执行。请谨记,以上两种的迭代器方法(GetEnumerator)仅能用于使用数组来实现的集合类,对于使用链表来实现的集合类来说,它必须要重写GetEnumerator、Currnet、MoveNext。但原理是和上面讨论时相同。