20. 枚举数和迭代器

来源:互联网 发布:大疆 知乎 编辑:程序博客网 时间:2024/06/11 03:42

20.1 枚举数和可枚举类型

使用foreach语句

当我们为数组使用foreach语句时,这个语句为我们依次取出了数组中的每一个元素,允许我们读取它的值。

为什么数组可以这么做?看上去很神奇。原因是数组可以按需提供一个叫做枚举数(enumerator)的对象。枚举数可以依次返回请求的数组的元素。枚举数“知道”项的次序并且跟踪它在序列中的位置,然后返回请求的当前项。

对于枚举数的类型而言,必须有一个方法来获取它们。在.NET中获取一个对象枚举数的标准方法是调用对象的GetEnumerator方法。实现GetEnumerator方法的类型叫做可枚举类型(enumerable type或enumerable)。数组是可枚举类型。


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

  • 通过调用GetEnumerator方法获取对象的枚举数。
  • 从枚举数中请求每一项并且把它作为迭代变量(iteration variable),代码可以读但不可以改变。

foreach(Type VarName in EnumerableObject)  //EnumerableObject必须是可枚举类型
{

......

}

枚举数类型

枚举数一共有三种。它们的工作原理本质上是一样的,只是有一些细小的区别。可以使用以下方式实现枚举数:

  • IEnumerator/IEnumerable接口——叫做非泛型接口形式。
  • IEnumerator<T>/IEnumerable<T>接口——叫做泛型接口形式。
  • 不使用接口形式。




20.2 使用IEnumerator接口

这种形式的枚举数是实现IEnumerator接口的类。之所以叫做非泛型是因为它没有使用C#泛型。

IEnumerator接口包含三个函数成员:Current、MoveNext以及Reset。

  • Current返回序列中当前位置项的属性
    • 它是只读属性
    • 它返回object类型的引用,所以可以返回任何类型。
  • MoveNext是把枚举数益前进到集合中下一项的方法。它也返回布尔值,指标新的位置是有效位置或已经超过了序列的尾部。
    • 如果新的位置有效的,方法返回true。
    • 如果新的位置是无效的(比如到达了尾部),方法返回了false。
    • 枚举数的原始位置在序列中的第一项之前。MoveNext必须在第一次使用Current之前使用,否则CLR会抛出一个InvalidOperationException异常。
  • Reset方法把位置重置为原始状态。
枚举数类通常被声明为类中的嵌套类。嵌套类是声明在另外一个类声明中的类。

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

例:

        static void Main(string[] args)        {            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接口有如下的特性:

  • 它是System.Collection命名空间的成员。
  • 它包含三个方法Current、MoveNext和Reset。




20.3 IEnumerable接口

IEnumerable接口只有一个成员——GetEnumerator方法,它返回对象的枚举数。


    using System.Collections;    class MyColors : IEnumerable    {        string[] Colors = { "Red", "Yellow", "Blue" };        public IEnumerator GetEnumerator()        {            return new ColorEnumerator(Colors);  //ColorEnumerator 枚举数的实例        }    }


使用Ienumerable和IEnumerator的示例

把MyColors和Colorenumerator示例放在一起,我们可以添加一个叫做Program的类,其中有一个Main方法用来创建MyColors的实例,并在foreach中使用。


using System;using System.Collections;namespace ConsoleApplication1{    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 { return Colors[Position]; }        }        public bool MoveNext()        {            if (Position < Colors.Length - 1)            {                Position++;                return true;            }            else                return false;        }        public void Reset()        {            Position=-1;        }    }    class MyColos : IEnumerable    {        string[] Colors = { "Red", "Yellow", "Blue" };        public IEnumerator GetEnumerator()        {            return new ColorEnumerator(Colors);        }    }    class Program    {        static void Main(string[] args)        {            MyColos mc = new MyColos();            foreach (string color in mc)            {                Console.WriteLine("{0}",color);            }        }    }}



20.4 不实现接口的枚举数

我们已经知道如何使用IEnumerable和IEnumerator接口来创建可枚举类型和枚举数,但是这种方法有几个缺点。

  • 首先,由Current返回的对象是Object类型的。对于值类型而言,在由Current返回之前必须装箱成object。在从Current获取之后,又必须再一次拆箱。如果需要操作大量的数据,会带来严重的性能问题。
  • 非泛型接口方法的另外一个缺点是失去了类型安全。值被作为对象来枚举,所以可以是任何类型。这就消除了编译时的类型检测。
我们可以通过对枚举数和可枚举类型类的声明做如下改变来解决这个问题。
  • 对于枚举类:
    • 不要继承自IEnumerator。
    • 像以前一样实现MoveNext。
    • 像以前一样实现Current,把返回类型设置为和枚举的项一样。
    • 不需要实现Reset。
  • 对于可枚举类:
    • 不要继承自IEnumerable
    • 像以前一样实现GetEnumerator,返回值为枚举数类。
下面展示了它们的区别。左边是非泛型接口代码,右边是非接口代码。有了这样的修改,foreach语句可以完美处理集合而且还没有关面列出的一些缺点。

对于非接口的枚举娄的实现来说,一个可能的问题就是其他程序集的类型可能希望使用接口方法来实现枚举。如果这些类型尝试使用接口转换来获取类对象的枚举,可能就会找不到。

要解决这个问题,我们可以在同一类中实现两种形式。也就是说,在类级别创建Current、MoveNext、Reset和GetEnumerator的实现,并且也为它们创建显式接口实现。有了这两组实现,foreach和其他结构会调用类型安全的、有效的实现,而其他结构可以调用显式接口的实现。




20.5 泛型枚举接口

第三种形式的枚举数是使用泛型接口IEnumerable<T>和IEnumerator<T>。它们被叫做泛型是因为使用了C#泛型,使用方式和非泛型形式差不多。当然,这两者之间的差别如下所示.

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






20.6 IEnumerator<T>接口

IEnumerator<T>接口使用泛型来返回实际的类型,而不是object类型的对象。

IEnumerator<T>接口从另外两个接口继承——非泛型IEnumerable接口和IDisposable接口。所以,它实现了它们的成员。

  • 我们已经知道了非泛型接口IEnumerable和它的三个成员。
  • IDisposable接口只有一个叫做Dispose的类型为void的无参方法,它可以用于释放由类占据的非托管资源。
  • IEnumerator<T>接口本身只有一个Current方法,它返回衍生类型的项——不是object类型的项。
  • 由于IEnumerator<T>和IEnumerator都有一个叫做Current的成员,我们应该显式实现IEnumerator版本,然后在类中实现泛型版本。




20.7 IEnumerable<T>接口

泛型IEnumerable<T>接口与IEnumerable的非泛型版本很相似。泛型版本从IEnumerable继承,所以也必须实现IEnumerable接口。

  • 与IEnumerable差不多,泛型版本也包含了一个方法——GetEnumerator。然而,这个版本的GetEnumerator实现泛型IEnumerator<T>接口的类对象。
  • 由于类必须实现两个GetEnumerator方法,我们需要显式实现非泛型版本,并在类中实现泛型版本。




20.8 迭代器

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

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

  • 迭代器返回一个泛型枚举数,该枚举数返回三个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语句的代码块。下面三种类型的代码块中的任意一种都可以是迭代器块:

  • 方法主体
  • 访问器主体
  • 运算符主体
迭代器块被认为与其他代码块不同。其他块包含的语句被当作是命令式的。也就是说,代码块的第一个语句被执行然后是后面的语句,最后控制离开块。
另一方面,迭代器块不是需要在同一时间执行的一系列命令式命令,而是描述了希望编译器为我们创建的枚举数的行为。迭代器块中的代码描述了如何枚举元素。
迭代器块有两个特殊语句:
  • 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(string[] args)        {            MyClass mc = new MyClass();            foreach(string shade in mc)                Console.WriteLine(shade);        }    }


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

之前的救命创建的类包含两部分:产生枚举数的迭代器以及返回枚举数的GetEnumerator方法。在这个示例中,迭代器被用来创建可枚举类型,而不是枚举数。与之前的示例相比,本例有一些重要的不同:

  • 在之前的示例中,BlackAndwhite迭代器方法返回IEnumerator<string>,MyClass类通过返回由BlackAndWhite返回的对象来实现Getenumerator方法。
  • 在本例中,BlackAndwhite迭代器方法返回IEnumerable<string>而不是IEnumerator<string>。因此,MyClass首先调用BlackAndwhite方法获取它的可枚举类型对象,然后调用对象的GetEnumerator方法来获取它的结果,从而实现GetEnumerator方法。
  • 注意,在Main的foreach语句中,我们可以使用类的实例,也可以直接调用BlackAndWhite方法,因为它返回的是可枚举类型。两种方法如下:

    class MyClass    {        public IEnumerator<string> Getenumerator()        {            IEnumerable myEnumerable = BlackAndWhite();            return myEnumerable.GetEnumerator();     //返回枚举数        }        public IEnumerable<string> BlackAndWhite() //迭代器        {            yield return "black";            yield return "gray";            yield return "white";        }    }    class Program    {              static void Main(string[] args)        {            MyClass mc = new MyClass();            foreach(string shade in mc) //使用类对象                Console.WriteLine(shade);            foreach (string shade in mc.BlackAndWhite())  //使用类枚举数方法                Console.WriteLine("{0}", shade);        }    }




20.9 常见迭代器模式

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

  • 当我们实现返回枚举数的迭代器时,必须通过实现getEnumerator来让类可以被枚举,它返回由迭代器返回的枚举数。如下图左部分所示。



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



20.10 产生可枚举类型和枚举数

之前的示例使用的迭代器返回IEnumerator<T>或IEnumerable<T>。我们还可以创建迭代器来返回非泛型的版本。可以指定如下的返回类型:

  • IEnumerator<T>(泛型——替代T为实际类型);
  • IEnumerable<T>(泛型——替代T为实际类型);
  • IEnumerator(非泛型);
  • IEnumerable(非泛型);
对于这两个枚举数类型,编译器生成包含非泛型或泛型枚举数的嵌套类,它的行为由迭代器块指定。

对于两个可枚举类型,它做得更多。它产生一个即是可枚举类型又是枚举数的嵌套类。因此,这个类实现了枚举数和GetEnumerator方法。注意,GetEnumerator被作为嵌套类的一部分来实现,而不是封闭类的一部分。
下图演示了由上例中的可枚举类型迭代器产生的泛型可枚举类型。
  • 迭代器的代码在图的左边,并且可以看到返回类型是IEnumerable<string>。
  • 图的右边显示嵌套类型实现了IEnumerator<string>和IEnumerable<string>。




20.11 产生多个可枚举类型

在下面的示例中,ColorCollection类有两个可枚举类型的迭代器——一个以正序进行枚举,而另一个以逆序进行枚举。注意,尽管它有两个方法返回可枚举类型,类本身不是可枚举类型,因为它没有实现GetEnumerator。

using System;using System.Collections.Generic;namespace ColorCollectionIterator{    class ColorCollection    {        string[] Colors={"Red","Orange","Yellow","Green","Blue","Purple"};        public IEnumerable<string> Forward()        {            for(int i=0;i<Colors.Length;i++)                yield return Colors[i];        }        public IEnumerable<string> Reverse()        {            for(int i=Colors.Length-1;i>=0;i--)                yield return Colors[i];        }         }    class Program    {              static void Main(string[] args)        {           ColorCollection cc = new ColorCollection();            foreach(string color in cc.Forward())                Console.WriteLine("{0}",color);            Console.WriteLine("");;            foreach(string color in cc.Reverse())                Console.WriteLine("{0}",color);            Console.WriteLine("");;            //Skip the foreach and manually use the enumerable and enumerator.            IEnumerable<string> ieable = cc.Reverse();            IEnumerator<string> ieator = ieable.GetEnumerator();            while (ieator.MoveNext())                Console.WriteLine("{0}",ieator.Current);            Console.WriteLine("");        }    }}




20.12 产生多个枚举数

之前的示例使用迭代器来产生具有两个可枚举类型的类。本例演示两个方面的内容:第一,它使用迭代器来产生具有两个枚举数的类;第二,它演示了迭代器如何能实现为属性而不是方法。

这段代码声明了两个属性来定义两个不同的枚举数。GetEnumerator方法根据colorFlag布尔变量的值返回两个枚举数的一个或另外一个。如果colorFlag为true,则返回Colors枚举数,否则,返回BlackAndWhite枚举数。

    class MyClass:IEnumerable<string>    {        bool ColorFlag = true;        public MyClass(bool flag)        {            ColorFlag = flag;        }        IEnumerator<string> BlackAndWhite    //属性—枚举数迭代器        {            get            {                yield return "black";                yield return "gray";                yield return "white";            }        }        IEnumerator<string> Colors        {            get            {                string[] theColors = { "blue", "red", "yellow" };                for (int i = 0; i < theColors.Length; i++)                    yield return theColors[i];            }        }        public IEnumerator<string> GetEnumerator()        {            return ColorFlag                            ? Colors                            : BlackAndWhite;        }        System.Collections.IEnumerator.GetEnumerator()        {             return ColorFlag                            ? Colors                            : BlackAndWhite;                   }             }       class Program    {              static void Main(string[] args)        {           MyClass  mc1 = new MyClass(true);            foreach(string color in mc1)                Console.WriteLine("{0}",color);            Console.WriteLine("");;            MyClass  mc2 = new MyClass(false);            foreach(string color in mc2)                Console.WriteLine("{0}",color);            Console.WriteLine("");        }    }





20.13 迭代器实质

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

  • 迭代器需要System.Collections.Generic命名空间,因此我们需要使用using指令包含它。
  • 在编译器生成的枚举数中,Reset方法没有实现。而它是接口需要的方法,因此调用实现时总是抛出System.NotSupportedException异常。
在后台,由编译器生成的枚举数类是有4个状态的状态机。
  • Before:首次调用MoveNext的初始状态。
  • Running:调用MoveNext后进入这个状态。在这个状态中,枚举数检测并设置下一项的位置。在遇到yield return、yield break或在迭代器体结束时,状态。
  • Suspended:状态机等待下次调用MoveNext状态。
  • After:没有更多项可以枚举。
如果状态机在before或suspended状态,并且有一次MoveNext方法调用,它就转到了running状态。在running状态中,它检测集合的下一项并设置位置。
如果有更多项,状态机会转入suspended状态,如果没有更多项,它转入并保持在after状态。



0 0