首页 > 代码库 > c# 自己实现可迭代的容器
c# 自己实现可迭代的容器
在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.Current80 {81 get82 {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的存在。
c# 自己实现可迭代的容器