枚举器和迭代器

枚举器和可枚举类型


第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. BZOJ 2244: [SDOI2011]拦截导弹 [CDQ分治 树状数组]

    传送门 题意:三维最长不上升子序列以及每个元素出现在最长不上升子序列的概率 $1A$了好开心 首先需要从左右各求一遍,长度就是$F[0][i]+F[1][i]-1$,次数就是$G[0][i]*G[1] ...

  2. JavaScript中涉及得运算符以及运算符的优先级

    在js中主要有三种运算符:算术运算符,逻辑与比较运算符,位运算符.在着三种运算符中,最常见的应该是算术与比较运算符,位运算符比较少见一些 *说到了运算符,就不得不说运算符的优先级.下面我来列一下这些运 ...

  3. 监督学习:随机梯度下降算法(sgd)和批梯度下降算法(bgd)

    线性回归 首先要明白什么是回归.回归的目的是通过几个已知数据来预测另一个数值型数据的目标值. 假设特征和结果满足线性关系,即满足一个计算公式h(x),这个公式的自变量就是已知的数据x,函数值h(x)就 ...

  4. ES6 学习笔记之二 块作用域与闭包

    "闭包是函数和声明该函数的词法环境的组合." 这是MDN上对闭包的定义. <JavaScript高级程序设计>中则是这样定义的:闭包是指有权访问另一个函数作用域中的变量 ...

  5. python爬虫(3)——SSL证书与Handler处理器

    一.SSL证书问题 上一篇文章,我们创建了一个小爬虫,下载了上海链家房产的几个网页.实际上我们在使用urllib联网的过程中,会遇到证书访问受限的问题. 处理HTTPS请求SSL证书验证,如果SSL证 ...

  6. 高可用Redis服务架构分析与搭建

    基于内存的Redis应该是目前各种web开发业务中最为常用的key-value数据库了,我们经常在业务中用其存储用户登陆态(Session存储),加速一些热数据的查询(相比较mysql而言,速度有数量 ...

  7. H5 拖拽,一个函数搞定,直接指定对象设置可拖拽

    页面上,弹个小窗体,想让它可以拖拽,又不想 加载一堆js,就简单的能让他可以拖动? 嗯,下面有这样一个函数,调用下就好了! 1. 先来说说 H5的 拖拽 在 HTML5 中,拖放是标准的一部分,任何元 ...

  8. Redis入门_上

    Redis是基于内存的Key-Value数据库,包含Set.String.SortedSet.List.Hash等数据结构,可用于缓存.排名.爬虫去重等应用场景. 1.思维导图 2.安装与配置 2.1 ...

  9. sphinx初识

    sphinx(SQL Phrase Index),查询词组索引. 定义:Sphinx是一个全文检索引擎. 特性: 1.高速索引 (在新款CPU上,近10 MB/秒); 2.高速搜索 (2-4G的文本量 ...

  10. 聚类-K均值

    数据来源:http://archive.ics.uci.edu/ml/datasets/seeds 15.26 14.84 0.871 5.763 3.312 2.221 5.22 Kama 14.8 ...