迭代器模式

迭代器是C#里面非常非常非常重要的一个概念,它是序列、LINQ等一系列概念的基础。

手写一个迭代器

class MyIEnumerable : IEnumerable<string>
{
public IEnumerator<string> GetEnumerator()
{
return new MyEnumerator();
} IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
} internal class MyEnumerator : IEnumerator<string>
{
private readonly List<string> _list; public string Current
{
get
{
if (_state == - || _state == _list.Count)
{
throw new InvalidOperationException();
}
return _list[_state];
}
} private int _state;
object IEnumerator.Current => Current;
public MyEnumerator()
{
_list = new List<string>() { "a", "b", "c", "d", "e", "f" };
_state = -;
}
public void Dispose()
{
Console.WriteLine("dispose");
}
public bool MoveNext()
{
if (_state != _list.Count)
{
_state++;
}
return _state < _list.Count;
}
public void Reset()
{
throw new NotImplementedException();
}
}
}

上面手写了一个迭代器,在这个迭代器内部嵌套了一个类实现IEnumerator。这是实现迭代器模式的C#标准代码。看起来很臃肿,如果我只想返回几个值,也必须这么写。

yield

C#利用这个关键字大大的简化了实现IEnumerable的步骤。但是编译器为我们在后台做的事情不少。我们慢慢来看。

C#2是利用一个关键字来告诉编译器去后台创建一个状态机。这个关键字是yield,比如下面的例子:

 class Program
{ static void Main(string[] args)
{
foreach (int item in GetEnumerable())
{
Console.WriteLine(item);
}
} static IEnumerable<int> GetEnumerable()
{
for (int i = ; i < ; i++)
{
yield return i;
}
} }

program类中声明了一个GetEnumerable的静态方法,返回一个IEnumerable<int>的实例。关键是在方法内部,出现了yield关键字,这就是告诉编译器要在后台生成一个类来实现迭代器模式,用ILSpy来看一下生成的这个类:

可以看到一个名字为“<GetEnumerable>d_1"的类被编译器生成了。因为我并没有声明这个类。这个类是一个嵌套类,在program类中定义。

这个类的声明如下:

private sealed class <GetEnumerable>d__1 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable

可以看到这个类实现了5个接口:IEnumerable<T>,IEnumerable,IEnumerator<T>,IEnumerator,IDisposable

这个类的结构如下:

它定义了三个字段并实现了接口的所有方法。关键的代码在MoveNext中,看一下:

可以看到在MoveNext中将GetEnumerable方法的逻辑放到了这里,而在GetEnumerable方法的内部,看一看到之前那一坨包含yield return的代码块不见了,而变成这样:

只有一句,就是将编译器生成的类实例化后返回。

具体在实例化这个<GetEnumerable>d__1实例时会传入-1还是-2取决于返回的是一个IEnumerable(-2)还是一个IEnumeartor(-1).

其他具体的可以用ILSpy来查看。ILSpy的地址在github上。

yield return需要注意的事项

从上面的描述可以得到如下信息:

  • 在返回一个IEnumerable或者IEnumerator或他们的泛型等价物的方法中执行yield,编译器就会在后台生成一个状态机,这个状态机实现了上面说的那5个接口IEnumerable<T>,IEnumerable,IEnumerator<T>,IEnumerator,IDisposable。这个状态机的代码和C#1中我们需要手写的实现迭代器的代码基本一样。
  • 如果方法声明的返回类型是非泛型接口, 那么迭代器块的生成类型(yield type) 是object, 否则就是泛 型接口的类型参数。 例如, 如果方法声明为返回IEnumerable<string>, 那么就会得到string类型的生成类型。

那么,如果要实现一个迭代器,就要考虑以下事情:

  1. 它必须具有某个初始状态,比如上面看到的那个生成类中的<>1__state;
  2. 每次调用MoveNext时, 在提供下一个值之前( 换句话说, 就是执行到yield return 语句之前), 它需要执行GetEnumerator 方法中的代码;
  3. 使用Current属性 时, 它必须返回我们生成的上一个值;
  4. 它必须知道何时完成生成值的操作, 以便MoveNext返回false。

对于如何解决第二点提出的问题,从上面的贴图来看,在MoveNext方法中会根据<>1__state的值进入到相应的代码段来执行相应的代码。

迭代器的工作流程

无论返回的是IEnumerable<T>还是IEnumerator<T>,在迭代器的方法里面没有什么区别,区别在于,如果你要枚举这个生成的类,比如说你要使用foreach来遍历,那么最好是返回一个IEnumerable的类型。因为在C#中是否能够遍历一个对象在于这个对象是否实现了一个GetEnumerator的方法(Duck Typing)。而无论它是否实现了那一堆接口。

用一个代码段来说明一下迭代器的执行流程吧:

 class Program
{
private static readonly string Padding = new string(' ', );
static void Main(string[] args)
{
IEnumerable<int> iterable = CreateEnumerable();
IEnumerator<int> iterator = iterable.GetEnumerator();
Console.WriteLine("start to iterate");
while (true)
{
Console.WriteLine("calling MoveNext()....");
bool moveNext = iterator.MoveNext();
Console.WriteLine($"....MoveNext result={moveNext}");
if (!moveNext)
{
break;
}
Console.WriteLine("fetching current value....");
Console.WriteLine($"current value={iterator.Current} ");
}
Console.ReadKey();
} static IEnumerable<int> CreateEnumerable()
{
Console.WriteLine($"{Padding} start of CreateEnumerable()");
for (int i = ; i < ; i++)
{
Console.WriteLine($"{Padding} about to yield {i}");
yield return i;
Console.WriteLine($"{Padding} after yield");
}
Console.WriteLine($"{Padding} yielding final value");
yield return -;
Console.WriteLine($"{Padding} End of CreateEnumerable()");
}
}

上面的代码会输出以下结果:

start to iterate
calling MoveNext()....
start of CreateEnumerable()
about to yield 0
....MoveNext result=True
fetching current value....
current value=0
calling MoveNext()....
after yield
about to yield 1
....MoveNext result=True
fetching current value....
current value=1
calling MoveNext()....
after yield
about to yield 2
....MoveNext result=True
fetching current value....
current value=2
calling MoveNext()....
after yield
yielding final value
....MoveNext result=True
fetching current value....
current value=-1
calling MoveNext()....
End of CreateEnumerable()
....MoveNext result=False

下面是结论:

  • 在第一次调用MoveNext之前, CreateEnumerable中的代码不会被调用。也就是说只有调用MoveNext的时候CreateEnumerable中的代码才会被调用。因为CreateEnumerable中的代码全部被搬到了MoveNext中,CreateEnumerable中的代码只剩下一行return new <GetEnumerable>d_1();
  • 所有工作在调用MoveNext时就完成了, 获取Current的值不会执行任何代码;
  • 在yield return的位置, 代码就停止执行, 在下一次调用MoveNext时又继续执行。MoveNext----yield return----MoveNext-----yield return循环直到MoveNext返回fanlse。
  • 在一个方法中的不同地方可以编写多个yield return语句;
  • 代码不会在最后的yield return处结束, 而是通过返回false的MoveNext调用来结束方法的执行。

遇到try--catch--final怎么办?

首先需要说明的是不能在try-catch块中使用yield return。编译器会提示两个:①不能在包含catch的try块中生成值②不能在catch块中生成值。这也就是说,出现yield关键字的地方不能有catch块。可以使用try--finally块。那么这个话题就变成了:在try--finally块中的执行情况

要离开作用域时,我们习惯用finally块去执行一些代码,它的语义是”最后无论如何都要执行“。

迭代器行为方式和普通的方法不太一样。yield return只是暂时停止了方法,而不是退出了方法,他的意思是说”我还会回来的“。而yield break语句的行为类似于普通的return--退出代码所在的方法。

说一个题外话:break是跳出当前循环,continue是结束一次循环 ,return是退出代码所在的方法。

在try--finally块中的执行情况的结论是无论是yield return还是yield break,在遇到finally的时候总会最终执行finally中的代码。原因是finally被编译器生成一个单独的方法,这个方法会在正常结束时被调用,也会在中途退出的时候被调用,上两张图:

这个是在实现IDisposable接口的方法,看到最下面那个了”<>m__Finally1“么?那是编译器生成的这个状态机的方法,前提如果我们的yield代码块中包含finally块时,这个finally块会被编译器编译成一个单独的方法。这个方法会在正常结束的地方放置,也会在Dispose方法中放置。而使用foreach调用时编译器会在foreach使用结束的时候调用状态机的Dispose方法。也就是说------只要调用者使用了foreach循环, 迭代器块中的finally将按照你期望的方式工作。如果你要想手动调用MoveNext方法和Current属性,那么请记得要将代码放到using语句中。using语句会帮助你调用Dispose。

现在总结以下编译器在实现状态机的时候一些要点:

  1. 在第一次调用MoveNext之前, Current属性总是返回迭代器产生类型的默认值;
  2. 在MoveNext返回false之后, Current属性总是返回最后的生成 值;
  3. Reset总是抛出异常, 而不像我们手动实现的Reset过程那样, 为了遵循语言规范, 这是必要的行为;
  4. 嵌套类总是实现IEnumerator的泛型形式和非泛型形式(提供给泛型和非泛型的IEnumerable所用)。

C#复习笔记(3)--C#2:解决C#1的问题(实现迭代器的捷径)的更多相关文章

  1. Java基础复习笔记系列 八 多线程编程

    Java基础复习笔记系列之 多线程编程 参考地址: http://blog.csdn.net/xuweilinjijis/article/details/8878649 今天的故事,让我们从上面这个图 ...

  2. Angular复习笔记7-路由(下)

    Angular复习笔记7-路由(下) 这是angular路由的第二篇,也是最后一篇.继续上一章的内容 路由跳转 Web应用中的页面跳转,指的是应用响应某个事件,从一个页面跳转到另一个页面的行为.对于使 ...

  3. Angular复习笔记7-路由(上)

    Angular复习笔记7-路由(上) 关于Angular路由的部分将分为上下两篇来介绍.这是第一篇. 概述 路由所要解决的核心问题是通过建立URL和页面的对应关系,使得不同的页面可以用不同的URL来表 ...

  4. Angular复习笔记6-依赖注入

    Angular复习笔记6-依赖注入 依赖注入(DependencyInjection)是Angular实现重要功能的一种设计模式.一个大型应用的开发通常会涉及很多组件和服务,这些组件和服务之间有着错综 ...

  5. tarjan复习笔记

    tarjan复习笔记 (关于tarjan读法,优雅一点读塔洋,接地气一点读塔尖) 0. 连通分量 有向图: 强连通分量(SCC)是个啥 就是一张图里面两个点能互相达到,那么这两个点在同一个强连通分量里 ...

  6. 树的直径,LCA复习笔记

    前言 复习笔记第6篇. 求直径的两种方法 树形DP: dfs(y); ans=max( ans,d[x]+d[y]+w[i] ); d[x]=max( d[x],d[y]+w[i] ); int di ...

  7. 状压DP复习笔记

    前言 复习笔记第4篇.CSP RP++. 引用部分为总结性内容. 0--P1433 吃奶酪 题目链接 luogu 题意 房间里放着 \(n\) 块奶酪,要把它们都吃掉,问至少要跑多少距离?一开始在 \ ...

  8. 斜率优化DP复习笔记

    前言 复习笔记2nd. Warning:鉴于摆渡车是普及组题目,本文的难度定位在普及+至省选-. 参照洛谷的题目难度评分(不过感觉部分有虚高,提高组建议全部掌握,普及组可以选择性阅读.) 引用部分(如 ...

  9. Java基础复习笔记系列 九 网络编程

    Java基础复习笔记系列之 网络编程 学习资料参考: 1.http://www.icoolxue.com/ 2. 1.网络编程的基础概念. TCP/IP协议:Socket编程:IP地址. 中国和美国之 ...

  10. Java基础复习笔记系列 七 IO操作

    Java基础复习笔记系列之 IO操作 我们说的出入,都是站在程序的角度来说的.FileInputStream是读入数据.?????? 1.流是什么东西? 这章的理解的关键是:形象思维.一个管道插入了一 ...

随机推荐

  1. human pose estimation

    2D Pose estimation主要面临的困难:遮挡.复杂背景.光照.真实世界的复杂姿态.人的尺度不一.拍摄角度不固定等. 单人姿态估计 传统方法:基于Pictorial Structures, ...

  2. 《Java大学教程》—第17章 Java聚焦类框架

    由所有聚焦类构成,在java.util包中,包含三个重要接口:*    List列表:元素为单个对象,元素在列表中是有序.可重复*    Set集合:元素为单个对象,元素在集合中无序.不可重复*    ...

  3. Linux 简介(day1)

    一.Linux 诞生于1991年 二.创始人:林纳斯.托瓦茨(Linus Torvalds) 三.logo:企鹅 四.Linux完整系统包括 1.Linux kernel (Linux 内核) 2.f ...

  4. node.js—File System(文件系统模块)

    文件系统模块概述 该模块是核心模块,提供了操作文件的一些API,需要使用require导入后使用,通过 require('fs') 使用该模块 文件 I/O 是由简单封装的标准 POSIX 函数提供的 ...

  5. 为什么java的类是单继承的,接口是多继承的

    类 如果一个类继承了两个类,但是这两个类中有相同的方法,那么子类调用方法时,无法确定应该调用哪个父类的方法. [c++是多继承的] 接口 jdk1.7  接口可以多继承,是因为当接口中是抽象方法.不存 ...

  6. ELMO模型(Deep contextualized word representation)

    1 概述 word embedding 是现在自然语言处理中最常用的 word representation 的方法,常用的word embedding 是word2vec的方法,然而word2vec ...

  7. CROI R1

    $CROI$ $R1$ 今天参加了一场比赛,什么比赛呢?CROI. CROI是什么呢? $Challestend$ $Rehtorbegnaro$ $OI$.总的来说就是我们机房的一些神仙出的题啦. ...

  8. IPS简单使用方法

    转载:http://blog.csdn.net/zhou1862324/article/details/17512191 IPS(incident packaging service)是11G的新特性 ...

  9. mongodb创建用户(转发)

    参考文档: https://www.cnblogs.com/itxiongwei/p/5520863.html MongoDB 缺省是没有设置鉴权的,业界大部分使用 MongoDB 的项目也没有设置访 ...

  10. python的格式化输出

    Python的格式化输出有两种: 一.类似于C语言的printf的方法 二.类似于C#的方法