注:本篇文章着重讨论IEnumerable接口,故文中所说集合皆指非泛型集合。IEnumerable< T >部分打算放在第二篇讲。
在日常开发中,常使用集合来储存各种类型的数据。细心的同学可能会发现,那些我们常用的集合几乎都实现了IEnumerable接口。问题是对于很多新手来说不了解这个IEnumerable接口到底是用来做什么的?试着回想一下,我们通常是如何从集合中获取数据的?当然是遍历集合对不对。那么如何执行遍历操作呢?你肯定会说使用foreach语句。是的,foreach语句是我们遍历数据集合最常用的方式之一,而正是它与IEnumerable接口有着非常紧密的联系。可笑的是,foreach语句作为从C#1时代就已经存在的语法,长久以来一直被我理所当然的使用着,从来没关心过foreach语句是如何实现遍历的。所以,我想用这篇文章来讨论我所面临的问题:
1、foreach是如何对集合进行迭代访问的呢?
很显然,集合并不是天生就支持遍历(迭代)的,而是继承了IEnumerable接口并实现了该接口所包含的唯一一个方法GetEnumerator()。换句话说,凡是实现了IEnumerable接口的类型,都是可以被迭代访问的。foreach语句实现遍历的功能实际上就是通过调用集合的GetEnumerator()方法来实现的,而这个过程实际上是在编译过程中进行的,这一点可以通过工具查看编译代码来验证。
首先,我们具体来看看IEnumerable接口:
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
可以看到IEnumerable接口仅拥有一个返回类型为IEnumerator的GetEnumerator()方法。似乎看起来很简单,但这个IEnumerator又是什么鬼?好吧,其实它就是我们所说的迭代器。那么既然foreach通过调用GetEnumerator()方法的目的是获取迭代器,想必每次从集合中迭代出的数据也一定存在迭代器中吧。抱着这样的疑问,我们再深入一点,看一下迭代器到底是什么东西:
public interface IEnumerator
{
bool MoveNext();
object Current{get;}
void Reset();
}
这里列出了迭代器最主要的属性Current和两个方法MoveNext()和Reset()。实际上,foreach调用GetEnumerator()方法获取迭代器的目的正是为了取到IEnumerator.Current属性。也就是说,Current属性保存了我们每次遍历出的集合项。而MoveNext()方法的作用获取下一个集合项的索引,如果索引值大于集合内数据的数量,则会返回false,循环就结束了。
我们可以试着分解一下foreach语句的执行过程:
第一步:
箭头移动到了集合上,这个过程编译器调用了集合的GetEnumerator()方法获取迭代器。
第二步:
箭头移动到了“in”上,这个过程编译器调用了迭代器的MoveNext()方法,使“指针”移动的下一个集合项上。
第三步:
此时,编译器调用迭代器的Current属性将值赋给了obj。
2、如何手写一个可迭代的自定义集合类型?
对于这个问题,我们通过一个代码片段来了解一下迭代器的实现方式。
代码如下:
//代码段1
public class Student
{
public string Name { get; set; }
public int Age { get; set; }
}
//代码段2
public class Students : IEnumerable
{
private Student[] arrStudent;
public Students(Student[] collection)
{
arrStudent = collection;
}
public Student this[int index]
{
get { return arrStudent[index]; }
}
public int Count
{
get { return arrStudent.Length; }
}
IEnumerator IEnumerable.GetEnumerator()
{
return new StudentIterator(this);
}
}
//代码段3
public class StudentIterator : IEnumerator
{
private Students arrStudents;
private int idx;
internal StudentIterator(Students collection)
{
this.arrStudents = collection;
idx = -1;
}
object IEnumerator.Current
{
get { return arrStudents[idx];}
}
public bool MoveNext()
{
idx++;
return idx < arrStudents.Count;
}
public void Reset()
{
idx = -1;
}
public void Dispose(){}
}
在代码段1中我们自定义了一个Student类来保存学生信息,接着在代码段2中我们又写一个Students类作为保存Student的集合类。我们要使Students能够被foreach语句所迭代,就必须要使Students继承IEnumerable接口并实现GetEnumerator()方法。因此,在代码段3中我们写了一个StudentIterator类继承并实现了IEnumerator接口,并将其作为GetEnumerator()方法的返回值。在代码段3中,我们定义了一个int类型的私有变量idx,这个变量就是充当了指针的作用。每次调用MoveNext()方法,idx就会加1,直到到达集合末尾为止。利用idx,我们就能够很容易的从集合中每次返回一个Student。需要注意的是,对于指针idx,我们通常将它的初始值设为-1。正如第一个问题中所说,编译时MoveNext()方法总是先于Current属性被调用,这样在第一个循环周期内调用MoveNext()方法将使idx的数值由-1变为0,这样就可以取到集合中索引为0的数据项。这在MSDN中关于IEnumerator接口的文档中也有所体现,原文如下:
Initially, the enumerator is positioned before the first element in the collection.You must call the MoveNext method to advance the enumerator to the first element of the collection before reading the value of Current; otherwise, Current is undefined.(初始时,枚举器位于集合中的第一个元素之前。您必须调用MoveNext方法将枚举器推送到集合的第一个元素,然后再读取Current的值; 否则Current将未定义。)
此时,我们已经完成了Students类可迭代的代码实现。在Main方法中写入如下代码,来查看一下效果。
Students arrStudent = new Students(
new Student[] {
new Student() { Name = "张三", Age = 23 },
new Student() { Name = "李四", Age = 24 },
new Student() { Name = "王五", Age = 45 },
new Student() { Name = "DDD", Age = 21 },
new Student() { Name = "BBB", Age = 14 },
new Student() { Name = "CCC", Age = 3 },
new Student() { Name = "AAA", Age = 22 },
new Student() { Name = "BBB", Age = 55 },
new Student() { Name = "CCC", Age = 43 },
});
foreach(Student obj in arrStudent)
{
Console.WriteLine("姓名:" + obj.Name + ",年龄:" + obj.Age);
}