详解c#迭代器
迭代器模式是设计模式中行为模式(behavioral pattern)的一个例子,他是一种简化对象间通讯的模式,也是一种非常容易理解和使用的模式。简单来说,迭代器模式使得你能够获取到序列中的所有元素 而不用关心是其类型是array,list,linked list或者是其他什么序列结构。这一点使得能够非常高效的构建数据处理通道(data pipeline)--即数据能够进入处理通道,进行一系列的变换,或者过滤,然后得到结果。事实上,这正是LINQ的核心模式。
在.NET中,迭代器模式被IEnumerator和IEnumerable及其对应的泛型接口所封装。如果一个类实现了IEnumerable接 口,那么就能够被迭代;调用GetEnumerator方法将返回IEnumerator接口的实现,它就是迭代器本身。迭代器类似数据库中的游标,他是 数据序列中的一个位置记录。迭代器只能向前移动,同一数据序列中可以有多个迭代器同时对数据进行操作。
在C#1中已经内建了对迭代器的支持,那就是foreach语句。使得能够进行比for循环语句更直接和简单的对集合的迭代,编译器会将 foreach编译来调用GetEnumerator和MoveNext方法以及Current属性,如果对象实现了IDisposable接口,在迭代 完成之后会释放迭代器。但是在C#1中,实现一个迭代器是相对来说有点繁琐的操作。C#2使得这一工作变得大为简单,节省了实现迭代器的不少工作。
接下来,我们来看如何实现一个迭代器以及C#2对于迭代器实现的简化,然后再列举几个迭代器在现实生活中的例子。
1. C#1:手动实现迭代器的繁琐
假设我们需要实现一个基于环形缓冲的新的集合类型。我们将实现IEnumerable接口,使得用户能够很容易的利用该集合中的所有元素。我们的忽 略其他细节,将注意力仅仅集中在如何实现迭代器上。集合将值存储在数组中,集合能够设置迭代的起始点,例如,假设集合有5个元素,你能够将起始点设为2, 那么迭代输出为2,3,4,0,最后是1. 为了能够简单展示,我们提供了一个设置值和起始点的构造函数。使得我们能够以下面这种方式遍历集合:
object[] values = { "a", "b", "c", "d", "e" };
IterationSample collection = new IterationSample(values, 3);
foreach (object x in collection)
{
Console.WriteLine(x);
}
由于我们将起始点设置为3,所以集合输出的结果是d,e,a,b及c,现在,我们来看如何实现 IterationSample 类的迭代器:
class IterationSample : IEnumerable
{
Object[] values;
Int32 startingPoint;
public IterationSample(Object[] values, Int32 startingPoint)
{
this.values = values;
this.startingPoint = startingPoint;
}
public IEnumerator GetEnumerator()
{
throw new NotImplementedException();
}
}
我们还没有实现GetEnumerator方法,但是如何写GetEnumerator部分的逻辑呢,第一就是要将游标的当前状态存在某一个地方。一方面 是迭代器模式并不是一次返回所有的数据,而是客户端一次只请求一个数据。这就意味着我们要记录客户当前请求到了集合中的那一个记录。C#2编译器对于迭代 器的状态保存为我们做了很多工作。 现在来看看,要保存哪些状态以及状态存在哪个地方,设想我们试图将状态保存在IterationSample集合中,使得它实现IEnumerator和 IEnumerable方法。咋一看,看起来可能,毕竟数据在正确的地方,包括起始位置。我们的GetEnumerator方法仅仅返回this。但是这 种方法有一个很重要的问题,如果GetEnumerator方法调用多次,那么多个独立的迭代器就会返回。例如,我们可以使用两个嵌套的foreach语 句,来获取所有可能的值对。这两个迭代需要彼此独立。这意味着我们需要每次调用GetEnumerator时返回的两个迭代器对象必须保持独立。我们仍旧 可以直接在IterationSample类中通过相应函数实现。但是我们的类拥有了多个职责,这位背了单一职责原则。因此,我们来创建另外一个类来实现 迭代器本身。我们使用C#中的内部类来实现这一逻辑。代码如下:
class IterationSampleEnumerator : IEnumerator
{
IterationSample parent;//迭代的对象 #1
Int32 position;//当前游标的位置 #2
internal IterationSampleEnumerator(IterationSample parent)
{
this.parent = parent;
position = -1;// 数组元素下标从0开始,初始时默认当前游标设置为 -1,即在第一个元素之前, #3
}
public bool MoveNext()
{
if (position != parent.values.Length) //判断当前位置是否为最后一个,如果不是游标自增 #4
{
position++;
}
return position < parent.values.Length;
}
public object Current
{
get
{
if (position == -1 || position == parent.values.Length)//第一个之前和最后一个自后的访问非法 #5
{
throw new InvalidOperationException();
}
Int32 index = position + parent.startingPoint;//考虑自定义开始位置的情况 #6
index = index % parent.values.Length;
return parent.values[index];
}
}
public void Reset()
{
position = -1;//将游标重置为-1 #7
}
}
要实现一个简单的迭代器需要手动写这么多的代码
- 需要记录迭代的原始集合#1
- 记录当前游标位置#2
- 返回元素时,根据 当前游标和数组定义的起始位置设置定迭代器在数组中的位置#6
- 初始化时,将当前位置设定在第一个元素之前#3
- 当第一次调用迭代器时首先需要调用 MoveNext,然后再调用Current属性。在游标自增时对当前位置进行条件判断#4
- 使得即使当第一次调用MoveNext时没有可返回的元素也 不至于出错#5
- 重置迭代器时,我们将当前游标的位置还原到第一个元素之前#7
除了结合当前游标位置和自定义的起始位置返回正确的值这点容易出错外,上面的代码非常直观。现在,只需要在IterationSample类的GetEnumerator方法中返回我们当才编写的迭代类即可:
public IEnumerator GetEnumerator()
{
return new IterationSampleEnumerator(this);
}
2. C#2:通过yield语句简化迭代
C#2使得迭代变得更加简单--减少了很多代码量也使得代码更加的优雅。下面的代码展示了再C#2中实现GetEnumerator方法的完整代码:
public IEnumerator GetEnumerator()
{
for (int index = 0; index < this.values.Length; index++)
{
yield return values[(index + startingPoint) % values.Length];
}
}
简单几行代码就能够完全实现IterationSampleIterator类所需要的功能。方法看起来很普通,除了使用了yield return。这条语句告诉编译器这不是一个普通的方法,而是一个需要执行的迭代块(yield block),他返回一个IEnumerator对象,你能够使用迭代块来执行迭代方法并返回一个IEnumerable需要实现的类型,IEnumerator或者对应的泛型。如果实现的是非泛型版本的接口,迭代块返的yield type是Object类型,否则返回的是相应的泛型类型。例如,如果方法实现IEnumerable接口,那么yield返回的类型就是String类型。 在迭代块中除了yield return外,不允许出现普通的return语句。块中的所有yield return 语句必须返回和块的最后返回类型兼容的类型。举个例子,如果方法定义需要返回IEnumeratble类型的话,不能yield return 1 。 需要强调的一点是,对于迭代块,虽然我们写的方法看起来像是在顺序执行,实际上我们是让编译器来为我们创建了一个状态机。这就是在C#1中我们书写的那部 分代码---调用者每次调用只需要返回一个值,因此我们需要记住最后一次返回值时,在集合中位置。 当编译器遇到迭代块是,它创建了一个实现了状态机的内部类。这个类记住了我们迭代器的准确当前位置以及本地变量,包括参数。这个类有点类似与我们之前手写 的那段代码,他将所有需要记录的状态保存为实例变量。下面来看看,为了实现一个迭代器,这个状态机需要按顺序执行的操作:
它需要一些初始的状态;
- 它需要一些初始的状态;
- 当MoveNext被调用时,他需要执行GetEnumerator方法中的代码来准备下一个待返回的数据;
- 当调用Current属性是,需要返回yielded的值;
- 需要知道什么时候迭代结束是,MoveNext会返回false。
2.2 迭代器的执行流程
如下的代码,展示了迭代器的执行流程,代码输出(0,1,2,-1)然后终止。
class Program {
static readonly String Padding = new String(' ', 30);
static IEnumerable<int32> CreateEnumerable()
{
Console.WriteLine("{0} CreateEnumerable()方法开始", Padding);
for (int i = 0; i < 3; i++)
{
Console.WriteLine("{0}开始 yield {1}", i);
yield return i;
Console.WriteLine("{0}yield 结束", Padding);
}
Console.WriteLine("{0} Yielding最后一个值", Padding);
yield return -1;
Console.WriteLine("{0} CreateEnumerable()方法结束", Padding);
}
static void Main(string[] args)
{
IEnumerable<int32> iterable = CreateEnumerable();
IEnumerator<int32> iterator = iterable.GetEnumerator();
Console.WriteLine("开始迭代");
while (true)
{
Console.WriteLine("调用MoveNext方法……");
Boolean result = iterator.MoveNext();
Console.WriteLine("MoveNext方法返回的{0}", result);
if (!result)
{
break;
}
Console.WriteLine("获取当前值……");
Console.WriteLine("获取到的当前值为{0}", iterator.Current);
}
Console.ReadKey();
}
}
从输出结果中可以看出一下几点:
- 直到第一次调用MoveNext,CreateEnumerable中的方法才被调用。
- 在调用MoveNext的时候,已经做好了所有操作,返回Current属性并没有执行任何代码。
- 代码在yield return之后就停止执行,等待下一次调用MoveNext方法的时候继续执行。
- 在方法中可以有多个yield return语句。
- 在最后一个yield return执行完成后,代码并没有终止。调用MoveNext返回false使得方法结束。
第一点尤为重要:这意味着,不能在迭代块中写任何在方法调用时需要立即执行的代码--比如说参数验证。如果将参数验证放在迭代块中,那么他将不能够很好的起作用,这是经常会导致的错误的地方,而且这种错误不容易发现。下面来看如何停止迭代,以及finally语句块的特殊执行方式。
2.3 迭代器的特殊执行流程
在普通的方法中,return语句通常有两种作用,一是返回调用者执行的结果。二是终止方法的执行,在终止之前执行finally语句中的方法。在上面的例子中,我们看到了yield return语句只是短暂的退出了方法,在MoveNext再次调用的时候继续执行。在这里我们没有写finally语句块。如何真正的退出方法,退出方法时finnally语句块如何执行,下面来看看一个比较简单的结构:yield break语句块。 使用 yield break 结束一个迭代
static IEnumerable<int32> CountWithTimeLimit(DateTime limit)
{
try
{
for (int i = 1; i <= 100; i++)
{
if (DateTime.Now >= limit)
{
yield break;
}
yield return i;
}
}
finally
{
Console.WriteLine("停止迭代!"); Console.ReadKey();
}
}
static void Main(string[] args)
{
DateTime stop = DateTime.Now.AddSeconds(2);
foreach (Int32 i in CountWithTimeLimit(stop))
{
Console.WriteLine("返回 {0}", i);
Thread.Sleep(300);
}
}
打个广告,Asp.Net 高级技术群 89336052
详解c#迭代器的更多相关文章
- 详解C# 迭代器[转]
迭代器模式是设计模式中行为模式(behavioral pattern)的一个例子,他是一种简化对象间通讯的模式,也是一种非常容易理解和使用的模式.简单来说,迭代器模式使得你能够获取到序列中的所有元素 ...
- 05 详解C# 迭代器
迭代器模式是设计模式中行为模式(behavioral pattern)的一个例子,他是一种简化对象间通讯的模式,也是一种非常容易理解和使用的模式. 简单来说,迭代器模式使得你能够获取到序列中的所有元素 ...
- day13 for内部机制详解,迭代器
迭代器定义: 可迭代协议:含有iter方法的都是可以迭代的 迭代器协议: 有.next 方法,和iter的都是迭代器 必须存在终结 特点: 节省空间 方便逐个取值,一个迭代器只能取一次 简单来说:满足 ...
- [No0000153]详解C# 迭代器【转】
迭代器模式是设计模式中行为模式(behavioral pattern)的一个例子,他是一种简化对象间通讯的模式,也是一种非常容易理解和使用的模式.简单来说,迭代器模式使得你能够获取到序列中的所有元素而 ...
- Hashmap 详解和迭代器问题
重点介绍HashMap.首先介绍一下什么是Map.在数组中我们是通过数组下标来对其内容索引的,而在Map中我们通过对象来对对象进行索引,用来索引的对象叫做key,其对应的对象叫做value.在下文中会 ...
- c/c++ 标准库 插入迭代器 详解
标准库 插入迭代器 详解 插入迭代器作用:copy等函数不能改变容器的大小,所以有时copy先容器是个空的容器,如果不使用插入迭代器,是无法使用copy等函数的. 例如下面的代码就是错误的: list ...
- python生成器详解
1. 生成器 利用迭代器(迭代器详解python迭代器详解),我们可以在每次迭代获取数据(通过next()方法)时按照特定的规律进行生成.但是我们在实现一个迭代器时,关于当前迭代到的状态需要我们自己记 ...
- 黑马----JAVA迭代器详解
JAVA迭代器详解 1.Interable.Iterator和ListIterator 1)迭代器生成接口Interable,用于生成一个具体迭代器 public interface Iterable ...
- [js高手之路] es6系列教程 - 迭代器,生成器,for...of,entries,values,keys等详解
接着上文[js高手之路] es6系列教程 - 迭代器与生成器详解继续. 在es6中引入了一个新的循环结构for ....of, 主要是用来循环可迭代的对象,那么什么是可迭代的对象呢? 可迭代的对象一般 ...
随机推荐
- ShellExecuteA
//第三个参数是指令,可以是一个可执行程序(后面不能加参数).有默认打开方式的文件.路径.网址.各种协议地址如迅雷ftp邮箱ed2k等 MessageBoxA
- nfs:环境搭建
准备环境 通过VirtualBox创建两台虚拟机client1和client2,这两台虚拟机和物理主机组成一个网络.将物理主机作为NFS的服务端,虚拟机client1和client2作为NFS的客户端 ...
- [PHP] Xhprof 非侵入式使用指南
一般使用 Xhprof ,按文档操作可以快速上手,文件头开启 Xhprof,应用结束处得到访问的url查看. 这种使用方式可以快速看到效果,同时也有一些不好的地方: 一是不利于重复利用写好的示例代码: ...
- KMP详解
原文: http://blog.csdn.net/v_july_v/article/details/7041827 从头到尾彻底理解KMP 1. 引言 本KMP原文最初写于2年多前的2011年12月, ...
- 替换CENTOS自带的yum源为网易163镜像源
首先确保你的系统是centos5或者centos6 先备份你系统自带的repo mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/Cent ...
- 19. UIAlertController 提示框获取文本内容,打印控制台上
1.首先定义一个全局字符串变量,方便接收获取的文本内容 2. -(void)viewDidAppear:(BOOL)animated{ UIAlertController * alert = [UIA ...
- myeclipse 10打开status.xml 卡死
myeclipse里面的struts.xml的插件格式和要打开的文件格式不一样. 右键struts.xml > Open With > MyEclipse XML Edito ...
- lua 位运算
bit = {data32={}} , do bit.data32[i] = ^(-i) end function bit:d2b( arg ) local num = tonumber( arg ) ...
- PHP 定界符使用
在PHP代码中,如果不想一行一行的拼接HTML或者JS的话,那么使用定界符将是最好的帮手! 使用方法: <<<eof .......html/js..... eof; 注意事项:(别 ...
- 卸载AppDomain动态调用DLL异步线程执行失败
应用场景 动态调用DLL中的类,执行类的方法实现业务插件功能 使用Assembly 来实现 但是会出现逻辑线程数异常的问题 使用AppDomain 实现动态调用,并卸载. 发现问题某个插件中开启异步线 ...