由于本建议涉及集合的遍历,所以在开始讲解本建议之前,我们不妨来设想一下如何对集合进行遍历。假设存在一个数组,其遍历模式可能采用依据索引来进行遍历的方法;又假设存在一个HashTable,其遍历模式可能是按照键值来进行遍历。无论是哪个集合,如果它们的遍历没有一个公共的接口,那么客户端在进行调用的时候,相当于是对具体类型进行了编码。这样一来,当需求发生变化时,就必须修改我们的代码。并且,由于客户端代码过多地关注了集合内部的实现,代码的可移植性就会变得很差,这直接违反了面向对象中的开闭原则。于是,迭代器模式就诞生了。现在,不要管FCL中是如何实现该模式的,我们先来实现一个自己的迭代器模式。
static void Main(string[]args)
{
//使用接口IMyEnumerable代替MyList
IMyEnumerable list=new MyList();
//得到迭代器,在循环中针对迭代器编码,而不是集合MyList
IMyEnumerator enumerator=list.GetEnumerator();
for(int i=0;i<list.Count;i++)
{
object current=enumerator.Current;
enumerator.MoveNext();
}
while(enumerator.MoveNext())
{
object current=enumerator.Current;
}
}
///<summary>
///要求所有的迭代器全部实现该接口
///</summary>
interface IMyEnumerator
{
bool MoveNext();
object Current{get;}
}
///<summary>
///要求所有的集合实现该接口
///这样一来,客户端就可以针对该接口编码
///而无需关注具体的实现
///</summary>
interface IMyEnumerable
{
IMyEnumerator GetEnumerator();
int Count{get;}
}
class MyList:IMyEnumerable
{
object[]items=new object[10];
IMyEnumerator myEnumerator;
public object this[int i]
{
get{return items[i];}
set{this.items[i]=value;}
}
public int Count
{
get{return items.Length;}
}
public IMyEnumerator GetEnumerator()
{
if(myEnumerator==null)
{
myEnumerator=new MyEnumerator(this);
}
return myEnumerator;
}
}
class MyEnumerator:IMyEnumerator
{
int index=0;
MyList myList;
public MyEnumerator(MyList myList)
{
this.myList=myList;
}
public bool MoveNext()
{
if(index+1>myList.Count)
{
index=1;
return false;
}
else
{
index++;
return true;
}
}
public object Current
{
get{return myList[index-1];}
}
}
MyList模拟了一个集合类,它继承了接口IMyEnumerable,这样,在客户端进行调用的时候,我们就可以直接使用IMyEnumerable来声明变量,如代码中的以下语句:
IMyEnumerable list=new MyList();
如果未来我们新增了其他的集合类,那么针对list的编码即使不做修改也能运行良好。在IMyEnumerable中声明的GetEnumerator方法返回一个继承了IMyEnumerator的对象。在MyList的内部,默认返回MyEnumerator。MyEnumerator就是迭代器的一个实现,如果对于迭代的需求有变化,可以重新开发一个迭代器(如下所示),然后在客户端迭代的时候使用该迭代器。
MyEnumerator enumerator2=new MyEnumerator(list);
while(enumerator2.MoveNext())
{
object current=enumerator2.Current;
}
注意,在客户端的代码中,我们在迭代的过程分别演示了for循环和while循环;但是因为使用了迭代器的缘故,两个循环都没有针对MyList编码,而是实现了对迭代器的编码。
理解了自己实现的迭代器模式,就相当于理解了FCL中提供的对应模式。以上代码中,在接口和类型名称中都加入了"My"字样,其实,FCL中有与之相对应的接口和类型,只不过为了演示的需要,增删了其中的部分内容,但是大致思路是一样的。使用FCL中相应的类型进行客户端的代码编写,大致应该像下面这样:
ICollection<object>list=new List<object>();
IEnumerator enumerator=list.GetEnumerator();
for(int i=0;i<list.Count;i++)
{
object current=enumerator.Current;
enumerator.MoveNext();
}
while(enumerator.MoveNext())
{
object current=enumerator.Current;
}
但是,无论是for循环还是while循环,都有些啰嗦,于是,foreach就出现了。在上面的代码中,如果循环用foreach代替会是什么样子?代码如下所示:
foreach(var current in list)
{
//省略了object current=enumerator.Current;
}
可以看到,采用foreach最大限度地简化了代码。它用于遍历一个继承了IEmuerable或IEmuerable<T>接口的集合元素。借助于IL代码,我们来查看使用foreach到底发生了什么事情:
.maxstack 1
.locals init([0]class[mscorlib]System.Collections.Generic.List1<object>list,
[1]valuetype[mscorlib]System.Collections.Generic.List1/
Enumerator<object>CS$5$0000)
IL_0000:newobj instance void class
[mscorlib]System.Collections.Generic.List1<object>::.ctor()
IL_0005:stloc.0
IL_0006:ldloc.0
IL_0007:callvirt instance valuetype
[mscorlib]System.Collections.Generic.List1/Enumerator<!0>class
[mscorlib]System.Collections.Generic.List1<object>::GetEnumerator()
IL_000c:stloc.1
.try
{
IL_000d:br.s IL_0017
IL_000f:ldloca.s CS$5$0000
IL_0011:call instance!0 valuetype[mscorlib]
System.Collections.Generic.List1/Enumerator<object>::get_Current()
IL_0016:pop
IL_0017:ldloca.s CS$5$0000
IL_0019:call instance bool valuetype[mscorlib]
System.Collections.Generic.List1/Enumerator<object>::MoveNext()
IL_001e:brtrue.s IL_000f
IL_0020:leave.s IL_0030
}//end.try
finally
{
IL_0022:ldloca.s CS$5$0000
IL_0024:constrained.valuetype[mscorlib]
System.Collections.Generic.List1/Enumerator<object>
IL_002a:callvirt instance void[mscorlib]System.IDisposable::Dispose()
IL_002f:endfinally
}//end handler
IL_0030:ret
在IL代码的开始处,可以看到声明了一个Enumerator的变量:CS$5$0000。在整个代码执行过程中,虽然C#代码中看不到针对集合对象的方法调用,但是研究IL代码就可以看出,运行时还是会调用集合类型的get_Current和MoveNext方法:
IL_0019:call instance bool valuetype
[mscorlib]System.Collections.Generic.List1/Enumerator<object>::MoveNext()
IL_001e:brtrue.s IL_000f
在调用完MoveNext方法后,如果结果是true,跳转到循环开始处。实际上,foreach循环和如下的while循环是一样的:
while(enumerator.MoveNext())
{
object current=enumerator.Current;
}
继续分析IL代码我们还可以知道,foreach循环除了可以提供简化的语法外,还有另外两个优势:
❑自动将代码置入try-finally块。
❑若类型实现了IDispose接口,它会在循环结束后自动调用Dispose方法。
如果用Reflector进行反编译,则对应的C#代码如下所示:
List<object>list=new List<object>();
using(List<object>.Enumerator CS$5$0000=list.GetEnumerator())
{
while(CS$5$0000.MoveNext())
{
object current=CS$5$0000.Current;
}
}
注意 using是try-finally的语法糖。上面的代码等同于:
List<object>.Enumerator CS$5$0000=list.GetEnumerator();
try
{
//循环
}
finally
{
CS$5$0000.Dispose();
}