基础【循环】------(枚举数和可枚举类型)------(摘抄)

1,使用foreach语句

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

1 例:声明一个有4个元素的数组,使用foreach循环打印这些项的值:
2 int[] arr1={10,11,12,3};
3 foreach(int item in arr1)
4      Console.WriteLine("Item value: {0},item");

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

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

可枚举类型和枚举数之间的关系如下图:

                                 

 

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

      * 通过调用GetEnumerator方法获取对象的枚举数;

      * 从枚举数中请求每一项并且把它作为迭代变量(iteration variable),代码可以读但不可以改变。

2,枚举数类型

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

     * IEnumerator/IEnumerable接口——叫做非泛型接口形式;

     * IEnumerator<T>/IEnumerable<T> 接口——叫做泛型接口形式;

     * 不使用接口形式;

      (1)使用IEnumerator接口

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

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

        * Current返回序列中当前位置项的属性。

               ~它是只读属性。

               ~它返回object类型的引用,所以可以返回任何类型。

        * MoveNext是把枚举数位置前进到集合中下一项的方法。它也返回布尔值,指示新的位置是有效位置或已经超过了序列的尾部。

                ~如果新的位置是有效的,方法返回true。

                ~如果新的位置是无效的(比如到达了尾部),方法返回false.   

                ~枚举数的原始位置在序列中的第一项之前。MoveNext必须在第一次使用Current之前使用。

       * Reset方法把位置重置为原始状态。

          下图左部显示了3个项的集合,右边显示了枚举数。枚举数是一个叫做ArrEnumerator类的实例。

       枚举数通常被声明为类中的嵌套类。嵌套类是声明在另外一个类声明中的类。

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

 

上图为有3个项的集合的枚举数的状态。

注意: * 在状态1中,枚举数的原始位置是-1 (也就是在集合的第一个元素之前)。

          * 状态的每次切换都由MoveNext进行,它提升了序列中的位置。每次调用MoveNext时,状态1到状态4都返回true,然而,在从状态4到状态5的过程中,位置最终超过了集合的最后一项,所以方法返回false.

          * 在最后一个状态中,任何进一步的调用MoveNext总是会返回false.

        有了集合的枚举数,我们就可以使用MoveNext和Current成员来模仿foreac循环遍历集合中的项。例如数组就是可枚举类型,所以下面的代码手动做foreach语句自动做的事情。事实上, 

 1 static void Main()
 2 {
 3      int[] MyArray={10,11,12,13};          //创建数组
 4    
 5     IEnumerator ie= MyArray.GetEnumerator();   //获取枚举数
 6     
 7     while(ie.MoveNext())
 8       {
 9              int i=(int) ie.Current;             //获取当前项
10              Console.WriteLine("{0}",i);    //输出
11        }
15 }    

输出:   11

           12

           13

           14 

(2)声明IEnumator的枚举数

       要创建非泛型接口的枚举数类我们必须声明实现IEnumerator接口的类。IEnumerator接口有如下的特性:

        * 它是System.Collections命名空间的成员;

        * 它包含3个方法Current,MoveNext和Reset.

        如下代码演示了非泛型枚举数类的框架。这里没显示是如何保持位置的。注意,Current返回object的引用。

    例如,下面代码实现了一个列出颜色名数组的枚举数类:

     

 1 using System.Collections;
 2 
 3 class ColorEnumerator : IEnumerator
 4 {
 5 
 6      string[] Colors ;     
 7      int Position =-1;
 8       
 9      public object Current 
10       {
11             get
12                 {
13                      if(Position==-1)
14                      throw new InvaliOperationException();
15                      if(Position==Colors.Length)
16                      throw new InvalidOperationException();
17          
18                       return   Colors[Position];
19 20         
21       }
22       public bool MoveNext()
23       {
24 
25           if(Position<Colors.Length-1)
26             {
27                    Position++;
28                    return true;
29              }
30              else
31                    return   false;
32        }
33           
34         public void  Reset()
35          {
36            Position=-1;
37            }
38           public ColorEnumerator(string[] theColors)   //
39            {
40                 Colors=new string[theColors.Length];
41                 for(int i=0; i<theColors.Length ; i++)
42                        Colors[i]=theColors[i];
43             }
44 
45 }

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

     

上图演示了一个有3个要枚举项的类MyClass,通过实现GetEnumerator方法来实现IEnumerable接口。

如下代码演示了可枚举类型的声明形式:

如下的代码给出了一个使用之前实例中的ColorEnumerator枚举数类的示例。要记住,ColorEnumerator实现了IEnumerator.

使用IEnumerable和IEnumerator的示例

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

   

(4)不实现接口的枚举数

       使用IEnumerable和IEnumerator接口来创建可枚举类型和枚举数,但是这种方法偶缺点:

             首先,由Current返回的对象是object类型的。对于值类型而言,在由Current返回之前必须装箱成object。在从Current获取之后,又必须再一次拆箱。如果需要操作大量的数据,会带来严重的性能问题。

            非泛型接口方法的另外一个缺点是失去了类型安全。值被作为对象来枚举,所以可以是任何类型,这就是消除了编译时的类型检测。

             可以通过对枚举数和可枚举类型的声明做如下改变来解决这个问题。

             *对于枚举数类:

              ~不要继承自IEnumerator;

              ~像以前一样实现MoveNext;

              ~像以前一样实现Current,把返回类型设置为和枚举的项一样。

              ~不需要实现Reset;

             * 对于可枚举类:

              ~ 不要继承自IEnumerable;

              ~ 像以前一样实现GetEnumerator,设置返回值为枚举数类。

 

上图左边是非泛型接口代码,右边是非接口代码。有了这样的修改,foreach语句可以完美处理集合而且还没有前面列出的一些缺点。对于非接口的枚举数的实现来说,一个可能的问题就是其他程序集的类型可能希望使用接口方法来实现枚举。如果这些类型尝试使用接口转换来获取类对象的枚举,可能就会找不到。

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

(5)泛型枚举接口

        第三种形式的枚举数是使用泛型接口IEnumerable<T>和IEnumerator<T>。它们被叫做泛型是因为使用了C#泛型,使用方式和非泛型形式差不多。

        * 对于非泛型接口形式:

       ~ IEnumerable接口的GetEnumerator方法返回实现IEnumerator枚举数类的实例。

       ~ 实现IEnumerator的类实现了Current属性,它返回object的引用,然后我们必须把它转化为实际类型的对象。

       * 对于泛型接口形式:

       ~ IEnumerable<T>接口的GetEnumerator方法返回实现IEnumerator<T>枚举数类的实例。

       ~ 实现IEnumerator<T>的类实现了Current属性,它返回实际类型的对象,而不是object基类的引用。

需要重点注意的是,非泛型接口的实现不是类型安全的。它们返回object类型的引用,然后需要转化为实际类型,而泛型接口的枚举数是类型安全的,它返回实际类型的引用。

(6)IEnumerator<T>接口

       IEnumerator<T>接口使用泛型来返回实际的类型,而不是object类型的引用。
       IEnumerator<T>接口从另外两个接口继承——非泛型IEnumerator接口和IDisposable接口。所以,它肯定实现了它们的成员。

        ~ 我们已经知道了非泛型接口IEnumrator和它的3个成员。

        ~ IDisposable接口只有一个叫做Dispose的类型为void的无参方法,它可以用于释放由类占据的非托管资源。

        ~ IEnumerator<T>接口本身只有一个Current特性,它返回类型T或它衍生的项的实例——不是object类型的引用。

        ~ 由于IEnumerator<T>和IEnumerator都有一个叫做Current的成员,我们应该显示实现IEnumerator版本,然后在类中实现泛型版本。

例如,如下代码使用泛型枚举数接口啦实现ColorEnumerator实例:

(7)IEnumerable<T>接口

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

        ~ 与IEnumerable差不多,泛型版本也包含了一个方法——GetEnumerator。然而,这个版本的GetEnumerator实现泛型IEnumerator<T>接口的类对象。

        ~ 由于类必须实现两个GetEnumerator方法,我们需要显示实现非泛型版本,并在类中实现泛型版本。

如下代码给出了实现泛型接口的模式。T是由枚举数返回的类型。

如下代码演示了泛型可枚举接口的使用:

 (8)迭代器

      可枚举类型和枚举数在.NET集合类中被广泛使用。C#从2.0版本开始提供了更简单的创建枚举数和可枚举类型的方式。编译器会为我们创建它们。这种结构叫做迭代器。我们可以把手动编码的可枚举类型和枚举数替换为由迭代器生产的可枚举类型和枚举数。 

      下面方法声明实现了一个产生和返回枚举数的迭代器。

       * 迭代器返回一个泛型枚举数,该枚举数返回3个string类型的项。

       * yield return语句声明这是枚举中的下一项。

   

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

 第一个版本中,如果方法在第一个yield return语句处返回,那么后两条语句永远不会到达。如果没有在第一条语句中返回,而是继续后面额代码,在这些值上发生了什么呢?在第二个版本中,如果循环主体中的yield return语句在第一个迭代中返回,循环永远不会获得其他的后续迭代。除此之外,枚举数不会一次返回所有元素—每次访问Current属性时便返回一个新值。     

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

     ~ 方法主体;

     ~ 访问器主体;

     ~ 运算符主体;

     迭代器块被认为与其他代码块不同。其他块包含的语句被当作是命令式的。也就是说,代码块的第一个语句被执行,然后是后面的语句,最后控制离开块。

      另一方面,迭代器块不是需要在同一时间执行的一序列命令式命令,而是描述了希望编译器为我们创建的枚举数类的行为。迭代器块中的代码描述了如何枚举元素。

      迭代器块有两个特殊语句。

      ~ yield return 语句执行了序列中返回的下一项。

      ~ yield break 语句指定在序列中没有更多项。

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

                                                          

使用迭代器来创建枚举数

      下面代码演示了如何使用迭代器来创建可枚举类型的类。

       ~ MyClass使用迭代器方法BlackAndWhite来产生迭代数。

       ~ MyClass还实现了GetEnumerator方法,它调用BlackAndWhite并且返回BlackAndWhite给它的枚举数。

       ~ 注意Main方法,由于类是可枚举类型,我们在foreach语句中直接使用了类的实例。

         

             

  

   下图左边演示了MyClass的代码,在右边演示了产生的对象。

         ~ 图中左边的迭代器代码演示了它返回类型是IEnumerator<string>.

         ~ 图中右边演示了它有一个嵌套类实现了IEnumerator<string>   

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

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

       ~ 在之前的示例中,BlackAndWhite迭代器方法返回IEnumerator<string>而不是IEnumerator<string>。因此,MyClass首先调用BlackAndWhite方法获取它的可枚举类型对象,然后调用对象的GetEnumerator语句中,我们可以使用类的实例,也可以直接调用BlackAndWhite方法,因为它返回的是可枚举类型。两种方法如下:

 

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

     ~ 图中左边的迭代器代码演示了它的返回类型是IEnumerable<string>.

     ~ 图中右边演示了它有一个嵌套类实现了IEnumerator<string>和IEnumerable<string>

      ~图中左边的迭代器代码演示了它的返回类型是IEnumerable<string>.

      ~图中右边演示了它有一个嵌套类实现了IEnumerator<string>和IEnumerable<string>。

(9)常见迭代器模式

        当我们实现返回枚举数的迭代器时,必须通过实现GetEnumerator来让类可以被枚举,它返回由迭代器返回的枚举数。

  如果我们在类中实现的迭代器返回可枚举类型,我们可以让类实现或不实现GetEnumerator来让类本身可被枚举或不可被枚举。

        ~如果实现GetEnumerator,让它调用迭代器方法以获取自动生成的实现IEnumerable的类实例。然后,从IEnumerator使类本身不可被枚举,如图右边所示。

        ~如果通过不实现GetEnumerator使类本身不可被枚举,仍然可以使用由迭代器返回的可枚举类,只需要直接调用迭代器方法,如图右边第二个foreach语句所示。

  (10)产生可枚举类型和枚举数

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

         ~IEnumerator<T>(泛型——替代T为实际类型)
         ~IEnumerable<T>(泛型——替代T为实际类型)

         ~IEnumerator(非泛型)

         ~IEnumeralbe(非泛型)

          对于这两个枚举数类型,编译器生成包含非泛型或泛型枚举数的嵌套类,它的行为由迭代器块指定。

          对于两个可枚举类型,它做得更多。它产生一个即是可枚举类型又是枚举数的嵌套类。因此,这个类实现了枚举数和GetEnumerator方法。注意,GetEnumerator被作为嵌套类的一部分来实现,而不是封闭类的一部分。

(11)产生多个可枚举数类型

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

 

(12)产生多个枚举数

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

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

 

(13)迭代器实质

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

        ~迭代器需要System.Collections.Generic命名空间,因此我们需要使用using指令包含它。

        ~在编译器生成的枚举数中,Reset方法没有实现。而它是接口需要的方法,因此调用实现时总是抛出Sysetm.NotSupportedException异常。

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

       ~Before 首先调用MoveNext的初始状态。

       ~Running调用MoveNext后进入这个状态。在这个状态中,枚举数检测并设置下一项的位置。在遇到yield return,yield break或在迭代器体结束时,退出状态。

       ~Suspended 状态机等待下次调用MoveNext的状态。

       ~After没有更多项可以枚举。

       如果状态机在before或suspended状态,并且有一次MoveNext方法调用,它就转到了running状态。在running状态中,她检测集合的下一项并设置位置。

       如果有更多项,状态机会转入suspended状态,如果没有更多项,它转入并保持在after状态。

 

 

posted @ 2013-12-02 08:58  行进  阅读(274)  评论(0编辑  收藏  举报