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的容器。
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# 自己实现可迭代的容器的更多相关文章
- 【Python】【容器 | 迭代对象 | 迭代器 | 生成器 | 生成器表达式 | 协程 | 期物 | 任务】
Python 的 asyncio 类似于 C++ 的 Boost.Asio. 所谓「异步 IO」,就是你发起一个 IO 操作,却不用等它结束,你可以继续做其他事情,当它结束时,你会得到通知. Asyn ...
- C++STL模板库序列容器之List容器
目录 一丶List容器的存储结构 二丶丶STL中list容器的使用. 一丶List容器的存储结构 list容器底层是链表结构来维护的.跟vector不一样. vector是数组维护的.拥有连续内存.所 ...
- Ruby Enumerator的各种迭代
Enumerator迭代 Mix-in Enumerator获得的迭代方法: each_cons: each_slice: each_with_index: with_index: each_with ...
- C++ Primer 学习笔记_34_STL实践与分析(8) --引言、pair类型、关联容器
STL实践与分析 --引言.pair类型.关联容器 引言: 关联容器与顺序容器的本质差别在于:关联容器通过键[key]来存储和读取元素,而顺序容器则通过元素在容器中的位置顺序的存取元素. ma ...
- java并发容器
同步容器将所有对容器状态的访问都串行化,以实现线程安全性.这种方式的缺点是严重降低并发性.Java 5.0提供了多种并发容器来改进同步容器的性能.如ConcurrentHashMap代替同步且基于散列 ...
- String池与iterator对集合的迭代
一.静态导入 1. 导入类的静态属性 import static java.lang.System.out; out.println("haha"); 2. ...
- day11-Python运维开发基础(迭代器与可迭代对象、高阶函数)
1. 迭代器与可迭代对象 # ### 迭代器 """ 迭代器: 能被next方法调用,并且不断返回下一个值的对象,是迭代器(对象) 特征:迭代器会生成惰性序列,它通过计算 ...
- Java核心技术点之多线程
学习Java的同学注意了!!! 学习过程中遇到什么问题或者想获取学习资源的话,欢迎加入Java学习交流群,群号码:279558494 我们一起学Java! 本文主要从整体上介绍Java中的多线程技术, ...
- c++程序设计之编程思想
代码块愈小,代码的功能就愈容易管理,代码的处理和移动就愈轻松. 任何一个傻瓜都能写出计算机可以理解的代码,唯有写出人类容易理解的代码,才是优秀的程序员. 绝大多数情况下,函数应该放在它所使用的数据的所 ...
随机推荐
- Matlab Tricks(二十六)—— 置乱(随机化)与恢复(shuffle/permutation & restore)
x = 1:10; n = length(x); perm = randperm(n); x_perm = x(perm); % x_perm 表示置乱后的结果 x_ori(perm) = x_per ...
- 亲串 (hdu 2203 KMP)
亲串 Time Limit: 3000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others) Total Submiss ...
- 【v2.x OGE课程 15】 布局相关
1.父亲和儿子的关系 我们可以Entity类看到非常多parent(父)与child(子)这种字眼,这是游戏引擎中常有的概念,简单而言是一种has-a的关系 watermark/2/text/aHR0 ...
- aspnetboilerplate && .net core 使用原生sql
利用aspnetboilerplate提供的工具类IDbContextProvider private readonly IDbContextProvider<XXXDbContext> ...
- docker端口映射或启动容器时报错Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen
现象: [root@localhost ~]# docker run -d -p 9000:80 centos:httpd /bin/sh -c /usr/local/bin/start.shd5b2 ...
- 怎么样 javascript / js 在 建立map
设定map道路(它是非常有用的json履行) var a = {}; a["key1"] = "value1"; a["key2"] = & ...
- WPF元素绑定
原文:WPF元素绑定 数据绑定简介:数据绑定是一种关系,该关系告诉WPF从源对象提取一些信息,并用这些信息设置目标对象的属性.目标属性是依赖项属性.源对象可以是任何内容,从另一个WPF元素乃至ADO. ...
- Mvvm Light Toolkit 入门
原文:Mvvm Light Toolkit 入门 前言 之前学习UWP的时候就一直看到有关MVVM的资料但是一直没有系统的去学,最近正好有时间,特地来攻破这个点,顺便学习一下VS与GitHub的链接和 ...
- Android项目实战(四十四):Zxing二维码切换横屏扫描
原文:Android项目实战(四十四):Zxing二维码切换横屏扫描 Demo链接 默认是竖屏扫描,但是当我们在清单文件中配置横屏显示的时候: <activity android:name=&q ...
- 【shell】gerrit同步备份脚本的实现
本期分享下gerrit如何实现备份,由于之前的一台代码服务器直接down掉,所以为了以防万一,开始备份gerrit数据,有人说,gerrit不过是个审查代码的工具,备份这个做什么,git是分布式代码管 ...