在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的容器。

    public class SimpleList<TIn>
{
private static TIn[] _element;
private const int DefaultSize = ;
private int _currentIndex = -;
private int _allocSize;
private int _length;
public SimpleList()
{
_element = new TIn[DefaultSize];
_allocSize = DefaultSize; } public TIn this[int index] { get { return _element[index]; } set { _element[index] = value; } }
public void Add(TIn value)
{ _currentIndex++;
if (_currentIndex >= DefaultSize)
{
_allocSize = _allocSize * ;
TIn[] tmp = _element;
_element = new TIn[_allocSize];
tmp.CopyTo(_element, ); }
_element[_currentIndex] = value;
_length++;
}
public int Length { get { return _length; }} }

这个SimpleList没有实现IEnumerable接口,所以我们不能通过foreach来访问它,编译器会提示类型is not enumerable。但是我们实现了Indexer,所以可以通过for语句来访问。

下面给SimpleList增加IEnumerable接口的实现完整代码:

   public class SimpleList<TIn> : IEnumerable<TIn>
{
private static TIn[] _element;
private const int DefaultSize = ;
private int _currentIndex = -;
private int _allocSize;
private int _length;
public SimpleList()
{
_element = new TIn[DefaultSize];
_allocSize = DefaultSize; } public TIn this[int index] { get { return _element[index]; } set { _element[index] = value; } }
public void Add(TIn value)
{ _currentIndex++;
if (_currentIndex >= DefaultSize)
{
_allocSize = _allocSize * ;
TIn[] tmp = _element;
_element = new TIn[_allocSize];
tmp.CopyTo(_element, ); }
_element[_currentIndex] = value;
_length++;
}
public int Length { get { return _length; }} public IEnumerator<TIn> GetEnumerator()
{
return new Enumerator(this);
} IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public struct Enumerator : IEnumerator<TIn>
{
private SimpleList<TIn> list;
private int curIndex;
private TIn current; internal Enumerator(SimpleList<TIn> l)
{
list = l;
curIndex = ;
current = default (TIn);
}
public void Dispose()
{ } public bool MoveNext()
{
if (curIndex < list.Length)
{
current = list[curIndex];
curIndex++;
return true;
}
return false;
} public void Reset()
{
curIndex = ;
current = default (TIn);
} public TIn Current { get { return current; }} object IEnumerator.Current
{
get
{
if (curIndex == || curIndex == list.Length + )
{
throw new ArgumentException("curIndex");
}
return Current;
}
}
}
}

现在我们可以通过foreach来遍历SimpleList容器对象了。我们分别通过for和foreach来遍历:

  class Program
{
static void Main(string[] args)
{ SimpleList<int> sl = new SimpleList<int>();
sl.Add();
sl.Add();
sl.Add();
Console.WriteLine("for 遍历:");
for (int i = ; i < sl.Length; i++)
{
Console.WriteLine(sl[i]);
}
Console.WriteLine("for each 遍历:");
foreach (var v in sl)
{
Console.WriteLine(v); } }
}

程序运行结果:

三、总结

  通过以上的介绍我们实现迭代器对象首先是需要实现IEnumerate接口,其次为了遍历该对象中的元素我们需要实现IEnumerator接口,IEnumerate接口是为了获得Enumerator对象,只有获得了Enumerator对象我们才可以遍历集合的元素,这也是IEnumerate和IEnumerator的区别。IEnumerate接口告诉外界,该对象是可迭代的,具体如何迭代,是Enumerator接口实现的事情,因此,外界可以不需要知道Enumerator的存在。

  

c# 自己实现可迭代的容器的更多相关文章

  1. 【Python】【容器 | 迭代对象 | 迭代器 | 生成器 | 生成器表达式 | 协程 | 期物 | 任务】

    Python 的 asyncio 类似于 C++ 的 Boost.Asio. 所谓「异步 IO」,就是你发起一个 IO 操作,却不用等它结束,你可以继续做其他事情,当它结束时,你会得到通知. Asyn ...

  2. C++STL模板库序列容器之List容器

    目录 一丶List容器的存储结构 二丶丶STL中list容器的使用. 一丶List容器的存储结构 list容器底层是链表结构来维护的.跟vector不一样. vector是数组维护的.拥有连续内存.所 ...

  3. Ruby Enumerator的各种迭代

    Enumerator迭代 Mix-in Enumerator获得的迭代方法: each_cons: each_slice: each_with_index: with_index: each_with ...

  4. C++ Primer 学习笔记_34_STL实践与分析(8) --引言、pair类型、关联容器

    STL实践与分析 --引言.pair类型.关联容器 引言:     关联容器与顺序容器的本质差别在于:关联容器通过键[key]来存储和读取元素,而顺序容器则通过元素在容器中的位置顺序的存取元素. ma ...

  5. java并发容器

    同步容器将所有对容器状态的访问都串行化,以实现线程安全性.这种方式的缺点是严重降低并发性.Java 5.0提供了多种并发容器来改进同步容器的性能.如ConcurrentHashMap代替同步且基于散列 ...

  6. String池与iterator对集合的迭代

    一.静态导入 1.       导入类的静态属性 import static java.lang.System.out; out.println("haha"); 2.       ...

  7. day11-Python运维开发基础(迭代器与可迭代对象、高阶函数)

    1. 迭代器与可迭代对象 # ### 迭代器 """ 迭代器: 能被next方法调用,并且不断返回下一个值的对象,是迭代器(对象) 特征:迭代器会生成惰性序列,它通过计算 ...

  8. Java核心技术点之多线程

    学习Java的同学注意了!!! 学习过程中遇到什么问题或者想获取学习资源的话,欢迎加入Java学习交流群,群号码:279558494 我们一起学Java! 本文主要从整体上介绍Java中的多线程技术, ...

  9. c++程序设计之编程思想

    代码块愈小,代码的功能就愈容易管理,代码的处理和移动就愈轻松. 任何一个傻瓜都能写出计算机可以理解的代码,唯有写出人类容易理解的代码,才是优秀的程序员. 绝大多数情况下,函数应该放在它所使用的数据的所 ...

随机推荐

  1. 扪心自问,强大的UI框架,给我们带来了什么?(作者因此写了一个GuiLite)

    MFC, QT, MINIGUI, ANDROID, IOS,如果这些平台,你都使用过,在下真心佩服,也相信你对这篇文章最具有发言权,真心期待你的回复和建议. 这些著名的UI说出来都让人如雷贯耳,如果 ...

  2. C#中的String.Format介绍

    关键字:C# string.format作者:txw1958原文:http://www.cnblogs.com/txw1958/archive/2012/11/15/csharp-string_for ...

  3. vc有关 directx组态,和dxsdk_extras(directshow)

      2009-11-10 0:28 此文章:自己编写 转载于<汤姆&杰瑞> DirectShow 1 -- 下载与VC配置 1 DirectX SDK9 Directshow sd ...

  4. JPA示例项(J采纳PA的hibernate实现版本号)

    (1).JPA介绍: JPA全名Java Persistence API ,Java坚持API这是Sun公司Java EE 5规范中提出的Java持久化接口. JPA吸取了眼下Java持久化技术的长处 ...

  5. if-then和if-then-else声明

    1.使用if-then声明 结构化命令,主要类型为if-then声明.if-then例如,下面的语句格式: if command then commands fi 假设你在使用其它编程语言的if-th ...

  6. 读BeautifulSoup官方文档之与bs有关的对象和属性(1)

    自从10号又是5天没更, 是, 我再一次断更... 原因是朋友在搞python, 老问我问题, 我python也是很久没碰了, 于是为了解决他的问题, 我只能重新开始研究python, 为了快速找回感 ...

  7. wpf 路由事件 代码片段

    <?xml version="1.0" encoding="utf-8" ?> <CodeSnippets xmlns="http: ...

  8. 查看 Linux 发行版本的名称以及版本号

    查看LINUX发行版的名称及其版本号的命令: lsb_release -a cat /etc/redhat-release(针对redhat,Fedora)

  9. wpf自定义带刻度的柱状图控件

    效果图: 主要代码xaml: <UserControl x:Class="INSControls._01Conning.Steer.ConningSpeedBar" xmln ...

  10. Qt for windows消息循环、libqxt分析和wince快捷键处理

    Qt for windows消息循环.libqxt分析和wince快捷键处理 利用Qt做windows图形界面开发和MFC相比,个人感觉还是比较简单好用的:首先利用Designer工具搞个ui文件:然 ...