枚举器和迭代器

枚举器和可枚举类型


第12章中,我们看到可以用foreach语句遍历数组。在本章,我们会进一步探讨数组,来看看为什么它们可以被foreach语句处理。我们还会研究如何使用迭代器为用户自定义类增加该功能。

foreach语句

数组foreach语句为我们依次取出数组中的每个元素。

int[] arr1={,,,};
foreach(int item in arr1)
{
Console.WriteLine("Item value: {0}",item);
}

为什么数组可以这么做?因为数组可以按需提供一个叫做枚举器(enumerator)的对象。枚举器可以依次返回请求的数组中的元素。枚举器“知道”项的次序并且跟踪它在序列中的位置,然后返回请求的当前项。
对于由枚举器的类型,必须有一个方法来获取它。获取对象枚举器的方法是调用对象的GetEnumerator方法。实现GetEnumerator方法的类型叫做可枚举类型(enumerable type或enumerable)。数组是可枚举类型。

foreach结构设计用来和可枚举类型一起使用。只要给它的遍历对象是可枚举类型,它就会执行如下行为:

  • 调用GetEnumerator获取对象枚举器
  • 从枚举器中请求每一项并且把它作为迭代变量(iteration variable),代码可以读取该变量但不能改变

IEnumerator接口


实现了IEnumerator接口的枚举器包含3个函数成员:Current、MoveNext、Reset。

  • Current是返回序列中当前位置项的属性

    • 它是只读属性
    • 它返回object类型的引用,所以可以返回任何类型
  • MoveNext是把枚举器为知前进到集合中下一项的方法。它返回布尔值,指示新的位置是有效位置还是已超过序列尾部
    • 如果新位置有效,返回true
    • 如果新位置无效,返回false
    • 枚举器的原始位置在序列第一项之前,依次MoveNext必须在第一次使用Current前调用
  • Reset是把位置重置为原始状态的方法

枚举器与序列中的当前项保持联系的方式完全取决于实现。可以通过对象引用、索引值或其他方式来实现。对于内置的一维数组来说,就是用项的索引。

有了集合的枚举器,我们就可以使用MoveNext和Current成员来模仿foreach循环遍历集合中的项。
例:手动做foreach语句自动做的事情。

class Program
{
static void Main()
{
int[] MyArray={,,,};
IEnumerator ie=MyArray.GetEnumerator();
while(ie.MoveNext())
{
int i=(int)ie.Current;
Console.WriteLine("{0}",i);
}
}
}

IEnumerator接口
可枚举类是指实现了IEnumerator接口的类。IEnumerator接口只有一个成员–GetEnumerator方法,它返回对象的枚举器。

使用IEnumerable和IEnumerator的示例

下面是个可枚举类的完整示例,类名Spectrum,枚举器类为ColorEnumerator。

using System;
using System.Collections;
class ColorEnumerator:IEnumerator
{
string[] _colors;
int _position=-;
public ColorEnumerator(string[] theColors)
{
_colors=new string[theColors.Length];
for(int i=;i<theColors.Length;i++)
{
_colors[i]=theColors[i];
}
}
public object Current
{
get
{
if(_position==-||_position>=_colors.Length)
{
throw new InvalidOperationException();
}
return _colors[_position];
}
}
public bool MoveNext()
{
if(_position<_colors.Length-)
{
_position++;
return true;
}
else
{
return false;
}
}
public void Reset()
{
_position=-;
}
}
class Spectrum:IEnumerable
{
string[] Colors={"violet","blue","cyan","green","yellow","orange","red"};
public IEnumerator GetEnumerator()
{
return new ColorEnumerator(Colors);
}
}
class Program
{
static void Main()
{
var spectrum=new Spectrum();
foreach(string color in spectrum)
{
Console.WriteLine(color);
}
}
}

泛型枚举接口


目前我们描述的枚举接口都是非泛型版本。实际上,在大多数情况下你应该使用泛型版本IEnumerable<T>IEnumerator<T>。它们叫做泛型是因为使用了C#泛型(参见第17章),其使用方法和非泛型形式差不多。
两者间的本质差别如下:

  • 对于非泛型接口形式:

    • IEnumerable接口的GetEnumerator方法返回实现IEnumerator枚举器类的实例
    • 实现IEnumerator的类实现了Current属性,它返回object的引用,然后我们必须把它转化为实际类型的对象
  • 对于泛型接口形式:
    • IEnumerable<T>接口的GetEnumerator方法返回实现IEnumator<T>的枚举器类的实例
    • 实现IEnumerator<T>的类实现了Current属性,它返回实际类型的对象,而不是object基类的引用

需要重点注意的是,我们目前所看到的非泛型接口的实现不是类型安全的。它们返回object类型的引用,然后必须转化为实际类型。
泛型接口的枚举器是类型安全的,它返回实际类型的引用。如果要创建自己的可枚举类,应该实现这些泛型接口。非泛型版本可用于C#2.0以前没有泛型的遗留代码。
尽管泛型版本和非泛型版本一样简单易用,但其结构略显复杂。

迭代器


可枚举类和枚举器在.NET集合类中被广泛使用,所以熟悉它们如何工作很重要。不过,虽然我们已经知道如何创建自己的可枚举类和枚举器了,但我们还是很高兴听到,C#从2.0版本开始提供了更简单的创建枚举器和可枚举类型的方式。
实际上,编译器将为我们创建它们。这种结构叫做迭代器(iterator)。我们可以把手动编码的可枚举类型和枚举器替换为由迭代器生成的可枚举类型和枚举器。

在解释细节前,我们先看两个示例。下面的方法实现了一饿产生和返回枚举器的迭代器。

  • 迭代器返回一个泛型枚举器,该枚举器返回3个string类型的项
  • yield return语句声明这是枚举中的下一项
public IEnumerator<string>BlackAndWhite()
{
yield return "black";
yield return "gray";
yield return "white";
}

下面方法声明了另一个版本,并输出相同结果:

public IEnumerator<string>BlackAndWhite()
{
string[] theColors={"black","gray","white"};
for(int i=;i<theColors.Length;i++)
{
yield return theColors[i];
}
}

迭代器块

迭代器块是有一个或多个yield语句的代码块。下面3种类型的代码块中的任意一种都可以是迭代器块:

  • 方法主体
  • 访问器主体
  • 运算符主体

迭代器块与其他代码块不同。其他块包含的语句被当做命令式。即先执行代码块中的第一个语句,然后执行后面的语句,最后控制离开块。
另一方面,迭代器块不是需要在同一时间执行的一串命令式命令,而是描述了希望编译器为我们创建的枚举器类的行为。迭代器块中的代码描述了如何枚举元素。
迭代器块由两个特殊语句:

  • yield return语句指定了序列中返回的下一项
  • yield break语句指定在序列中没有的其他项

编译器得到有关枚举项的描述后,使用它来构建包含所有需要的方法和属性实现的枚举器类。结果类被嵌套包含在迭代器声明的类中。
如下图所示,根据迭代器块的返回类型,你可以让迭代器产生枚举器或可枚举类型。

使用迭代器来创建枚举器

class MyClass
{
public IEnumerator<string> GetEnumerator()
{
return BlackAndWhite(); //返回枚举器
}
public IEnumerator<string> BlackAndWhite()//迭代器
{
yield return "black";
yield return "gray";
yield return "white";
}
}
class Program
{
static void Main()
{
var mc=new MyClass();
foreach(string shade in mc)
{
Console.WriteLine(shade);
}
}
}

下图演示了MyClass的代码及产生的对象。注意编译器为我们自动做了多少工作。

  • 图左的迭代器代码演示了它的返回类型是IEnumerator<string>
  • 图右演示了它有一个嵌套类实现了IEnumerator<string>

使用迭代器来创建可枚举类型

之前示例创建的类包含两部分:产生返回枚举器方法的迭代器以及返回枚举器的GetEnumerator方法。
本节例子中,我们用迭代器来创建可枚举类型,而不是枚举器。与之前的示例相比,本例有以下不同:

  • 本例中,BlackAndWhite迭代器方法返回IEnumerable<string>而不是IEnumerator<string>。因此MyClass首先调用BlackAndWhite方法获取它的可枚举类型对象,然后调用对象的GetEnumerator方法来获取结果,从而实现GetEnumerator方法
  • 在Main的foreach语句中,我们可以使用类的实例,也可以调用BlackAndWhite方法,因为它返回的是可枚举类型
class MyClass
{
public IEnumerator<string> GetEnumerator()
{
IEnumerable<string> myEnumerable=BlackAndWhite();
return myEnumerable.GetEnumerator();
}
public IEnumerable<string> BlackAndWhite()//迭代器
{
yield return "black";
yield return "gray";
yield return "white";
}
}
class Program
{
static void Main()
{
var mc=new MyClass();
foreach(string shade in mc)
{
Console.Write(shade);
}
foreach(string shade in mc.BlackAndWhite)
{
Console.Write(shade);
}
}
}

下图演示了在代码的可枚举迭代器产生泛型可枚举类型。

  • 图左的迭代器代码演示了它的返回类型是IEnumerable<string>
  • 图右演示了它有一个嵌套类实现了IEnumerator<string>IEnumerable<string>

常见迭代器模式


前面两节展示了,我们可以创建迭代器来返回可枚举类型或枚举器。下图总结了如何使用普通迭代器模式。

  • 当我们实现返回枚举器的迭代器时,必须通过实现GetEnumerator来让类可枚举,如图左
  • 如果我们在类中实现迭代器返回可枚举类型,我们可以让类实现GetEnumerator来让类本身可被枚举,或不实现GetEnumerator,让类不可枚举
    • 若实现GetEnumerator,让它调用迭代器方法以获取自动生成的实现IEnumerable的类实例。然后从IEnumerable对象返回由GetEnumerator创建的枚举器,如图右
    • 若通过不实现GetEnumerator使类本身不可枚举,仍然可以使用由迭代器返回的可枚举类,只需要直接调用迭代器方法,如果右第二个foreach语句

产生多个可枚举类型


下例中,Spectrum类有两个可枚举类型的迭代器。注意尽管它有两个方法返回可枚举类型,但类本身不是可枚举类型,因为它没有实现GetEnumerator

using System;
using System.Collections.Generic;
class Spectrum
{
string[] colors={"violet","blue","cyan","green","yellow","orange","red"};
public IEnumerable<string> UVtoIR()
{
for(int i=;i<colors.Length;i++)
{
yield return colors[i];
}
}
public IEnumerable<string> IRtoUV()
{
for(int i=colors.Length-;i>=;i--)
{
yield return colors[i];
}
}
}
class Program
{
static void Main()
{
var spectrum=new Spectrum();
foreach(string color in spectrum.UVtoIR())
{
Console.Write(color);
}
Console.WriteLine();
foreach(string color in spectrum.IRtoUV())
{
Console.Write(color);
}
Console.WriteLine();
}
}

将迭代器作为属性


本例演示两个内容:第一,使用迭代器来产生具有两个枚举器的类;第二,演示迭代器如何实现属性。

using System;
using System.Collections.Generic;
class Spectrum
{
bool _listFromUVtoIR;
string[] colors={"violet","blue","cyan","green","yellow","orange","red"};
public Spectrum(bool listFromUVtoIR)
{
_listFromUVtoIR=listFromUVtoIR;
}
public IEnumerator<string> GetEnumerator()
{
return _listFromUVtoIR?UVtoIR:IRtoUV;
}
public IEnumera<string> UVtoIR
{
get
{
for(int i=;i<colors.Length;i++)
{
yield return colors[i];
}
}
}
public IEnumerable<string> IRtoUV
{
get
{
for(int i=colors.Length-;i>=;i--)
{
yield return colors[i];
}
}
}
}
class Program
{
static void Main()
{
var startUV=new Spectrum(true);
var startIR=new Spectrum(false);
foreach(string color in startUV)
{
Console.Write(color);
}
Console.WriteLine();
foreach(string color in startIR)
{
Console.Write(color);
}
Console.WriteLine();
}
}

迭代器实质


如下是需要了解的有关迭代器的其他重要事项。

  • 迭代器需要System.Collections.Generic命名空间
  • 在编译器生成的枚举器中,Reset方法没有实现。而它是接口需要的方法,因此调用时总是抛出System.NetSupportedException异常。

在后台,由编译器生成的枚举器类是包含4个状态的状态机。

  • Before 首次调用MoveNext的初始状态
  • Running 调用MoveNext后进入这个状态。在这个状态中,枚举器检测并设置下一项的为知。在遇到yield return、yield break或在迭代器体结束时,退出状态
  • Suspended 状态机等待下次调用MoveNext的状态
  • After 没有更多项可以枚举

如果状态机在Before或Suspended状态时调用MoveNext方法,就转到了Running状态。在Running状态中,它检测集合的下一项并设置为知。
如果有更多项,状态机会转入Suspended状态,如果没有更多项,它转入并保持在After状态。

C#图解教程 第十八章 枚举器和迭代器的更多相关文章

  1. 2017.4.12 开涛shiro教程-第十八章-并发登录人数控制

    原博客地址:http://jinnianshilongnian.iteye.com/blog/2018398 根据下载的pdf学习. 开涛shiro教程-第十八章-并发登录人数控制 shiro中没有提 ...

  2. C# 枚举器和迭代器

    一.枚举器(enumerator)和可枚举类型(enumeration) 我们都知道foreach语句可以用来遍历数组中的元素,但你有没有想过为什么它可以被foreach处理呢? 这是因为数组可以按需 ...

  3. 设计模式 - 适配器模式(adapter pattern) 枚举器和迭代器 具体解释

    适配器模式(adapter pattern) 枚举器和迭代器 具体解释 本文地址: http://blog.csdn.net/caroline_wendy 參考适配器模式(adapter patter ...

  4. C#-14 枚举器和迭代器

    一 枚举器和可枚举类型 当我们为数组使用foreach语句时,这个语句为我们依次取出了数组中的每一个元素. var arrInt = new int[] { 11, 12, 13, 14 }; for ...

  5. python 教程 第十八章、 Web编程

    第十八章. Web编程 import urllib2 LOGIN = 'jin' PASSWD = 'Welcome' URL = 'https://tlv-tools-qc:8443/qcbin/s ...

  6. 【Unity|C#】基础篇(20)——枚举器与迭代器(IEnumerable/IEnumerator)

    [学习资料] <C#图解教程>(第18章):https://www.cnblogs.com/moonache/p/7687551.html 电子书下载:https://pan.baidu. ...

  7. C#知识点-枚举器和迭代器

    一.几个基本概念的理解 问题一:为什么数组可以使用foreach输出各元素 答:数组是可枚举类型,它实现了一个枚举器(enumerator)对象:枚举器知道各元素的次序并跟踪它们的位置,然后返回请求的 ...

  8. C#图解教程 第十九章 LINQ

    LINQ 什么是LINQLINQ提供程序 匿名类型 方法语法和查询语法查询变量查询表达式的结构 from子句join子句什么是联结查询主体中的from-let-where片段 from子句let子句w ...

  9. C#图解教程 第二十四章 反射和特性

    反射和特性 元数据和反射Type 类获取Type对象什么是特性应用特性预定义的保留的特性 Obsolete(废弃)特性Conditional特性调用者信息特性DebuggerStepThrough 特 ...

随机推荐

  1. POJ 2409 Let it Bead [置换群 Polya]

    传送门 题意:$m$种颜色$n$颗珠子,定义旋转和翻转两种置换,求不等价着色数 暴力求每个置换的循环节也许会$T?$ 我们可以发现一些规律: 翻转: $n$为奇数时每个置换有$1+\frac{n-1} ...

  2. BZOJ 3963: [WF2011]MachineWorks [CDQ分治 斜率优化DP]

    传送门 当然了WF的题uva hdu上也有 你的公司获得了一个厂房N天的使用权和一笔启动资金,你打算在这N天里租借机器进行生产来获得收益.可以租借的机器有M台.每台机器有四个参数D,P,R,G.你可以 ...

  3. JavaWeb项目架构之NFS文件服务器

    NFS简介 NFS(Network File System)即网络文件系统. 主要功能:通过网络(局域网)让不同的主机系统之间可以共享文件或目录. 主要用途:NFS网络文件系统一般被用来存储共享视频, ...

  4. 简述java中equals()方法和==的区别

    ==与equals的主要区别是: ==: ==常用于比较原生类型(基本数据类型):byte,short,char,int,long,float,double,boolean,比较的是他们的值. 若用= ...

  5. 【Tools】ubuntu无法virtualenv创建python虚拟环境的解决

    刚有人问我Ubuntu python虚拟环境无法创建问题,报错same file error,防止今后遇到忘记,记录下可能的问题. 1.先在windows上试了下: pip install virtu ...

  6. go get报错unrecognized import path “golang.org/x/net/context”…

    今天安装gin框架,首先下载gin,命令如下:go get github.com/mattn/go-sqlite3 结果报错: package golang.org/x/net/context: un ...

  7. C控制语句:分支和跳转

    小技巧:程序return前加个getchar();可以让程序停住.%%可以打印使printf()中打印出%号 #include<stdio.h>#define SPACE ''int ma ...

  8. python爬取快手视频 多线程下载

    就是为了兴趣才搞的这个,ok 废话不多说 直接开始. 环境: python 2.7 + win10 工具:fiddler postman 安卓模拟器 首先,打开fiddler,fiddler作为htt ...

  9. kaggle-titanic 数据分析过程

    1. 引入所有需要的包 # -*- coding:utf-8 -*- # 忽略警告 import warnings warnings.filterwarnings('ignore') # 引入数据处理 ...

  10. struts 中的创建Action的三种方法

    1.对于直接创建类,不实现接口和继承任何的类 例如创建一个helloAction package cn.lonecloud.control; import com.opensymphony.xwork ...