C#图解教程 第十八章 枚举器和迭代器

枚举器和迭代器

枚举器和可枚举类型


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

foreach语句

数组foreach语句为咱们依次取出数组中的每一个元素。安全

int[] arr1={10,11,12,13};
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。spa

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

枚举器与序列中的当前项保持联系的方式彻底取决于实现。能够经过对象引用、索引值或其余方式来实现。对于内置的一维数组来讲,就是用项的索引。
设计

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

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

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

使用IEnumerable和IEnumerator的示例

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

using System;
using System.Collections;
class ColorEnumerator:IEnumerator
{
    string[] _colors;
    int _position=-1;
    public ColorEnumerator(string[] theColors)
    {
        _colors=new string[theColors.Length];
        for(int i=0;i<theColors.Length;i++)
        {
            _colors[i]=theColors[i];
        }
    }
    public object Current
    {
        get
        {
            if(_position==-1||_position>=_colors.Length)
            {
                throw new InvalidOperationException();
            }
            return _colors[_position];
        }
    }
    public bool MoveNext()
    {
        if(_position<_colors.Length-1)
        {
            _position++;
            return true;
        }
        else
        {
            return false;
        }
    }
    public void Reset()
    {
        _position=-1;
    }
}
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章),其使用方法和非泛型形式差很少。
二者间的本质差异以下:blog

  • 对于非泛型接口形式:
    • 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=0;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=0;i<colors.Length;i++)
        {
            yield return colors[i];
        }
    }
    public IEnumerable<string> IRtoUV()
    {
        for(int i=colors.Length-1;i>=0;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=0;i<colors.Length;i++)
            {
                yield return colors[i];
            }
        }
    }
    public IEnumerable<string> IRtoUV
    {
        get
        {
            for(int i=colors.Length-1;i>=0;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状态。

相关文章
相关标签/搜索