在c#中我们经常使用到foreach语句来遍历容器,如数组,List,为什么使用foreach语句能够遍历一个这些容器呢,首先的一个前提是这些容器都实现了IEnumerable接口,通过IEnumerable接口的GetEnumerator方法获得实现IEnumerator接口的对象。IEnumerator接口对象定义了两个方法外加一个属性。分别为MoveNext方法和Reset方法,以及Current属性。
一、foreach背后的故事
下面我们通过一个简单的例子来探索foreach背后究竟发生了什么,进而我们自己实现一个简单的可迭代的容器。
namespace CustomEnumerateTest { class Program { static void Main(string[] args) { List<int> l = new List<int>(); l.Add(1); l.Add(2); l.Add(3); foreach (var v in l) { Console.WriteLine(v); } } } }
这是一段很简单的代码,foreach究竟是如何来实现容器对象的遍历的,我们通过ILDasm工具来查看c#编译器生成的中间码。代码如下,只贴出部分中间码:
IL_0020: ldloc.0
IL_0021: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<int32>::GetEnumerator() //调用List的GetEnumerator方法获取GetEnumerator对象
IL_0026: stloc.2
.try { IL_0027: br.s IL_003a //跳转指令,跳转到IL_003a处执行 IL_0029: ldloca.s CS$5$0000 //获取Enumerator对象的地址,push到堆栈。 IL_002b: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::get_Current() //调用Enumerator对象的get_Current()方法。 IL_0030: stloc.1 IL_0031: nop IL_0032: ldloc.1 IL_0033: call void [mscorlib]System.Console::WriteLine(int32) IL_0038: nop IL_0039: nop IL_003a: ldloca.s CS$5$0000 IL_003c: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()//调用Enumerator对象的MoveNext()方法。 IL_0041: stloc.3 IL_0042: ldloc.3 IL_0043: brtrue.s IL_0029 IL_0045: leave.s IL_0056 } // end .try finally { IL_0047: ldloca.s CS$5$0000 IL_0049: constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> IL_004f: callvirt instance void [mscorlib]System.IDisposable::Dispose() IL_0054: nop IL_0055: endfinally } // end handler
上面贴出的就是对应源码中的foreach块的代码 。从红色部分我们可以看到了。首先编译器在遇到foreach语句时,会先调用List的GetEnumerator方法获得Enumerator对象,其中Enumerator对象实现了IEnumerator接口,GetEnumerator方法是IEnumerable接口,List实现了该接口。其次,编译器分别调用Enumerator对象的MoveNext方法,和Current方法获取对象的当前元素,这里是int类型的元素,不断循环直到条件为假,中间码IL_0043指令处的条件判断用于判断是否结束循环。
既然我们看到了foreach背后的真实的调用,那么现在我们自己实现一个简单的可迭代的容器,以便加深我们对IEnumerale和IEnumerator接口的理解。
二、简单的可迭代容器实现(本代码只是为了说明问题,对于各种异常情况没有做相应处理。)
首先来看一个没有实现IEnumerable的容器。
1 public class SimpleList<TIn> 2 { 3 private static TIn[] _element; 4 private const int DefaultSize = 5; 5 private int _currentIndex = -1; 6 private int _allocSize; 7 private int _length; 8 public SimpleList() 9 { 10 _element = new TIn[DefaultSize]; 11 _allocSize = DefaultSize; 12 13 } 14 15 public TIn this[int index] { get { return _element[index]; } set { _element[index] = value; } } 16 public void Add(TIn value) 17 { 18 19 _currentIndex++; 20 if (_currentIndex >= DefaultSize) 21 { 22 _allocSize = _allocSize * 2; 23 TIn[] tmp = _element; 24 _element = new TIn[_allocSize]; 25 tmp.CopyTo(_element, 0); 26 27 } 28 _element[_currentIndex] = value; 29 _length++; 30 } 31 public int Length { get { return _length; }} 32 33 }
这个SimpleList没有实现IEnumerable接口,所以我们不能通过foreach来访问它,编译器会提示类型is not enumerable。但是我们实现了Indexer,所以可以通过for语句来访问。
下面给SimpleList增加IEnumerable接口的实现完整代码:
1 public class SimpleList<TIn> : IEnumerable<TIn> 2 { 3 private static TIn[] _element; 4 private const int DefaultSize = 5; 5 private int _currentIndex = -1; 6 private int _allocSize; 7 private int _length; 8 public SimpleList() 9 { 10 _element = new TIn[DefaultSize]; 11 _allocSize = DefaultSize; 12 13 } 14 15 public TIn this[int index] { get { return _element[index]; } set { _element[index] = value; } } 16 public void Add(TIn value) 17 { 18 19 _currentIndex++; 20 if (_currentIndex >= DefaultSize) 21 { 22 _allocSize = _allocSize * 2; 23 TIn[] tmp = _element; 24 _element = new TIn[_allocSize]; 25 tmp.CopyTo(_element, 0); 26 27 } 28 _element[_currentIndex] = value; 29 _length++; 30 } 31 public int Length { get { return _length; }} 32 33 public IEnumerator<TIn> GetEnumerator() 34 { 35 return new Enumerator(this); 36 } 37 38 IEnumerator IEnumerable.GetEnumerator() 39 { 40 return GetEnumerator(); 41 } 42 public struct Enumerator : IEnumerator<TIn> 43 { 44 private SimpleList<TIn> list; 45 private int curIndex; 46 private TIn current; 47 48 internal Enumerator(SimpleList<TIn> l) 49 { 50 list = l; 51 curIndex = 0; 52 current = default (TIn); 53 } 54 public void Dispose() 55 { 56 57 58 } 59 60 public bool MoveNext() 61 { 62 if (curIndex < list.Length) 63 { 64 current = list[curIndex]; 65 curIndex++; 66 return true; 67 } 68 return false; 69 } 70 71 public void Reset() 72 { 73 curIndex = 0; 74 current = default (TIn); 75 } 76 77 public TIn Current { get { return current; }} 78 79 object IEnumerator.Current 80 { 81 get 82 { 83 if (curIndex == 0 || curIndex == list.Length + 1) 84 { 85 throw new ArgumentException("curIndex"); 86 } 87 return Current; 88 } 89 } 90 } 91 }
现在我们可以通过foreach来遍历SimpleList容器对象了。我们分别通过for和foreach来遍历:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 6 SimpleList<int> sl = new SimpleList<int>(); 7 sl.Add(1); 8 sl.Add(2); 9 sl.Add(3); 10 Console.WriteLine("for 遍历:"); 11 for (int i = 0; i < sl.Length; i++) 12 { 13 Console.WriteLine(sl[i]); 14 } 15 Console.WriteLine("for each 遍历:"); 16 foreach (var v in sl) 17 { 18 Console.WriteLine(v); 19 20 } 21 22 23 24 25 } 26 }
程序运行结果:
三、总结
通过以上的介绍我们实现迭代器对象首先是需要实现IEnumerate接口,其次为了遍历该对象中的元素我们需要实现IEnumerator接口,IEnumerate接口是为了获得Enumerator对象,只有获得了Enumerator对象我们才可以遍历集合的元素,这也是IEnumerate和IEnumerator的区别。IEnumerate接口告诉外界,该对象是可迭代的,具体如何迭代,是Enumerator接口实现的事情,因此,外界可以不需要知道Enumerator的存在。