迭代器模式是设计模式中行为模式(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, );
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 = -;// 数组元素下标从0开始,初始时默认当前游标设置为 -1,即在第一个元素之前, #3
} public bool MoveNext()
{
if (position != parent.values.Length) //判断当前位置是否为最后一个,如果不是游标自增 #4
{
position++;
}
return position < parent.values.Length;
} public object Current
{
get
{
if (position == - || 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 #7
}
}

要实现一个简单的迭代器需要手动写这么多的代码:需要记录迭代的原始集合#1,记录当前游标位置#2,返回元素时,根据 当前游标和数组定义的起始位置设置定迭代器在数组中的位置#6。初始化时,将当前位置设定在第一个元素之前#3,当第一次调用迭代器时首先需要调用 MoveNext,然后再调用Current属性。在游标自增时对当前位置进行条件判断#4,使得即使当第一次调用MoveNext时没有可返回的元素也 不至于出错#5。重置迭代器时,我们将当前游标的位置还原到第一个元素之前#7。 除了结合当前游标位置和自定义的起始位置返回正确的值这点容易出错外,上面的代码非常直观。现在,只需要在IterationSample类的GetEnumerator方法中返回我们当才编写的迭代类即可:

public IEnumerator GetEnumerator()
{
return new IterationSampleEnumerator(this);
}

值得注意的是,上面只是一个相对简单的例子,没有太多的状态需要跟踪,不用检查集合在迭代的过程中是否发生了变化。为了 实现一个简单的迭代器,在C#1中我们实现了如此多的代码。在使用Framework自带的实现了IEnumerable接口的集合时我们使用 foreach很方便,但是当我们书写自己的集合来实现迭代时需要编写这么多的代码。在C#1中,大概需要40行代码来实现一个简单的迭代器,现在看看 C#2对这一过程的改进。

2. C#2:通过yield语句简化迭代

2.1 引入迭代块(iterator)和yield return 语句

C#2使得迭代变得更加简单--减少了很多代码量也使得代码更加的优雅。下面的代码展示了再C#2中实现GetEnumerator方法的完整代码:

public IEnumerator GetEnumerator()
{
for (int index = ; index < this.values.Length; index++)
{
yield return values[(index + startingPoint) % values.Length];
}
}

简单几行代码就能够完全实现IterationSampleIterator类所需要的功能。方法看起来很普通,除了使用了yield return。这条语句告诉编译器这不是一个普通的方法,而是一个需要执行的迭代块(yield block),他返回一个IEnumerator对象,你能够使用迭代块来执行迭代方法并返回一个IEnumerable需要实现的类型,IEnumerator或者对应的泛型。如果实现的是非泛型版本的接口,迭代块返的yield typeObject类型,否则返回的是相应的泛型类型。例如,如果方法实现IEnumerable<string>接口,那么yield返回的类型就是String类型。 在迭代块中除了yield return外,不允许出现普通的return语句。块中的所有yield return 语句必须返回和块的最后返回类型兼容的类型。举个例子,如果方法定义需要返回IEnumeratble<string>类型的话,不能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(' ', );
static IEnumerable<int32> CreateEnumerable()
{
Console.WriteLine("{0} CreateEnumerable()方法开始", Padding);
for (int i = ; i &lt; ; i++)
{
Console.WriteLine("{0}开始 yield {1}", i);
yield return i;
Console.WriteLine("{0}yield 结束", Padding);
}
Console.WriteLine("{0} Yielding最后一个值", Padding);
yield return -;
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();
}
}

从输出结果中可以看出一下几点:

  • 直到第一次调用MoveNextCreateEnumerable中的方法才被调用。
  • 在调用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 = ; i &lt;= ; 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();
foreach (Int32 i in CountWithTimeLimit(stop))
{
Console.WriteLine("返回 {0}", i);
Thread.Sleep();
}
}

转载自:http://www.yamatamain.com/article/21/1.html

详解C# 迭代器[转]的更多相关文章

  1. 详解c#迭代器

    迭代器模式是设计模式中行为模式(behavioral pattern)的一个例子,他是一种简化对象间通讯的模式,也是一种非常容易理解和使用的模式.简单来说,迭代器模式使得你能够获取到序列中的所有元素 ...

  2. 05 详解C# 迭代器

    迭代器模式是设计模式中行为模式(behavioral pattern)的一个例子,他是一种简化对象间通讯的模式,也是一种非常容易理解和使用的模式. 简单来说,迭代器模式使得你能够获取到序列中的所有元素 ...

  3. day13 for内部机制详解,迭代器

    迭代器定义: 可迭代协议:含有iter方法的都是可以迭代的 迭代器协议: 有.next 方法,和iter的都是迭代器 必须存在终结 特点: 节省空间 方便逐个取值,一个迭代器只能取一次 简单来说:满足 ...

  4. [No0000153]详解C# 迭代器【转】

    迭代器模式是设计模式中行为模式(behavioral pattern)的一个例子,他是一种简化对象间通讯的模式,也是一种非常容易理解和使用的模式.简单来说,迭代器模式使得你能够获取到序列中的所有元素而 ...

  5. Hashmap 详解和迭代器问题

    重点介绍HashMap.首先介绍一下什么是Map.在数组中我们是通过数组下标来对其内容索引的,而在Map中我们通过对象来对对象进行索引,用来索引的对象叫做key,其对应的对象叫做value.在下文中会 ...

  6. c/c++ 标准库 插入迭代器 详解

    标准库 插入迭代器 详解 插入迭代器作用:copy等函数不能改变容器的大小,所以有时copy先容器是个空的容器,如果不使用插入迭代器,是无法使用copy等函数的. 例如下面的代码就是错误的: list ...

  7. python生成器详解

    1. 生成器 利用迭代器(迭代器详解python迭代器详解),我们可以在每次迭代获取数据(通过next()方法)时按照特定的规律进行生成.但是我们在实现一个迭代器时,关于当前迭代到的状态需要我们自己记 ...

  8. 黑马----JAVA迭代器详解

    JAVA迭代器详解 1.Interable.Iterator和ListIterator 1)迭代器生成接口Interable,用于生成一个具体迭代器 public interface Iterable ...

  9. [js高手之路] es6系列教程 - 迭代器,生成器,for...of,entries,values,keys等详解

    接着上文[js高手之路] es6系列教程 - 迭代器与生成器详解继续. 在es6中引入了一个新的循环结构for ....of, 主要是用来循环可迭代的对象,那么什么是可迭代的对象呢? 可迭代的对象一般 ...

随机推荐

  1. android的屏幕保持常亮

    1.Wake Lock是一种锁的机制 在Manifest.xml文件里面用user-permission声明.代码如下: 这种方法,在安装apk时,系统会提示安装人是否允许使用禁止休眠功能. < ...

  2. 异常处理——毕向东Java基础教程学习笔记

    1.异常:就是程序运行过程中出现的不正常情况. 异常的由来:问题本身也是日常生活中一个具体的事物,也可以通过java类的形式进行描述,并封装成对象.                        其实 ...

  3. setInterval setTimeout clearInterval

    setTimeout() 只执行 code 一次.如果要多次调用,请使用 setInterval() 或者让 code 自身再次调用 setTimeout(). //第一次load的时候就先刷新一次 ...

  4. PHP面试题集之基础题

    1.用PHP打印出前一天的时间格式是 2006-5-10 22:21:21 date_default_timezone_set('PRC'); //默认时区 echo "今天:", ...

  5. Report List Controls

    Report风格的ListCtrl的扩展,原文链接地址:http://www.codeproject.com/Articles/5560/Another-Report-List-Control 1.列 ...

  6. Windows 系统下json 格式的日志文件发送到elasticsearch

    Windows 系统下json 格式的日志文件发送到elasticsearch配置 Nxlog-->logstash-->ElasticSearch Logstash https://ww ...

  7. 虚拟机下Ubuntu没有GUI图形界面,解决方法

    先说下快捷键,CLI切换到GUI:Ctrl+Alt+F7: GUI切换到CLI:Ctrl+Alt+F1. 今天折腾虚拟机时,打开Ubuntu后显示的是命令行界面,按快捷键后并没转换到图形界面,而是一直 ...

  8. Canvas修行之黑客帝国代码雨

    既然是修行,不卖弄关子,不吊胃口,修行成果必须先晒一晒. 下图是我用canvas画的黑客帝国代码雨,想起当年看黑客帝国时,那个代码雨场景让我心旷神怡,大开脑洞,满脑子是那种三维空间,无数0和1像雨一样 ...

  9. LeetCode 2 Add Two Sum 解题报告

    LeetCode 2 Add Two Sum 解题报告 LeetCode第二题 Add Two Sum 首先我们看题目要求: You are given two linked lists repres ...

  10. 【Android 基础】Animation 动画介绍和实现

    在前面PopupWindow 实现显示仿腾讯新闻底部弹出菜单有用到Animation动画效果来实现菜单的显示和隐藏,本文就来介绍下吧. 1.Animation 动画类型 Android的animati ...