首页 > 代码库 > 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# 自己实现可迭代的容器