C# 迭代器、枚举器、IEnumerable和IEnumerator

开始之前先思考几个问题:

  • 为什么集合可以使用foreach来遍历
  • 不用foreach能不能遍历各元素
  • 为什么在foreach中不能修改item的值?
  • 要实现foreach需要满足什么条件?
  • 为什么Linq to Object中要返回IEnumerable?

一、枚举器和可枚举类型

1、什么是可枚举类型?

可枚举类是指实现了IEnumerable接口的类,比如数组就是可枚举类型;下面展示了一个可枚举类的完整示例:

namespace ConsoleApplication4
 {
     /// <summary>
     /// 自定义一个枚举对象
     /// </summary>
    class ColorEnumerator : IEnumerator
     {
         private string[] _colors;
         private int _position = -1;
 
         public ColorEnumerator(string[] arr)
         {
            _colors = arr;
             for (int i = 0; i < arr.Length; i++)
             {
                _colors[i] = arr[i];
             }
         }
         public object Current
         {
             get
             {
                 if (_position == -1)
                 {
                     throw new InvalidOperationException();
                 }
                 if (_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;
         }
     }
 
     /// <summary>
     /// 创建一个实现IEnumerable接口的枚举类
     /// </summary>
     class Spectrum : IEnumerable
     {
         private string[] Colors = { "red", "yellow", "blue" };
         public IEnumerator GetEnumerator()
         {
             return new ColorEnumerator(Colors);
         }
     }
 
     class Program
     {
         static void Main(string[] args)
         {
             Spectrum spectrum = new Spectrum();
            foreach (string color in spectrum)
             {
                Console.WriteLine(color);
            }
            Console.ReadKey();
         }
     }
}
View Code

2、什么是枚举器?

IEnumerable接口只有一个成员GetEnumerator方法,它返回的对象就是枚举器;实现了IEnumerator接口的枚举器包含三个函数成员:Current,MoveNext,Reset

  • Current是只读属性,它返回object类型的引用;
  • MoveNext是把枚举器位置前进到集合的下一项的方法,它返回布尔值,指示新的位置是否有效位置还是已经超过了序列的尾部;
  • Reset是把位置重置为原始状态的方法;

3、为什么集合可以使用foreach来遍历

我们知道当我们使用foreach语句的时候,这个语句为我们依次取出了数组中的每一个元素。

例如下面的代码:

int[] arr = { 1, 2, 3, 4, 5, 6 };
foreach( int arry in arr )
{
    Console.WriteLine("Array Value::{0}",arry);
}

输出效果为

为什么数组可以使用foreach来遍历?原因是数组可以按需提供一个叫做枚举器(enumerator)的对象,枚举器可以依次返回请求的数组中的元素,枚举器知道项的次序并且跟踪它在序列中的位置。依次返回请求的当前项。

对于有枚举器的类型而言,必须有一个方法来获取它这个类型。获取一个对象枚举器的方法是调用对象的GetEnumrator方法,实现GetEnumrator方法的类型叫做可枚举类型。那么数组就是可枚举类型。

总结来说,实现GetEnumrator方法的类型叫做可枚举类型,GetEnumrator方法返回的对象就是枚举器,枚举器可以依次返回请求的数组中的元素,枚举器知道项的次序并且跟踪它在序列中的位置。依次返回请求的当前项。

下图演示一下可枚举类型和枚举器之间的关系

foreach结构设计用来和可枚举类型一起使用,只要给它的遍历对象是可枚举类型,比如数组。基本逻辑如下:

  • 通过调用GetEnumrator方法获取对象的枚举器。
  • 从枚举器中请求每一项并且把它作为迭代器,代码可以读取该变量,但不可以改变
foreach(Type VarName in EnumrableObject )
{
}

EnumrableObjec必须是可枚举类型。

4、不用foreach能不能遍历各元素?

当然是可以的,看下面代码:

 

二、迭代器

设计模式中有个迭代器模式,其实这里说的迭代器就是利用迭代器设计模式实现的一个功能,返回的是枚举器。

1、自定义迭代器

.net中迭代器是通过IEnumerable和IEnumerator接口来实现的,换句话说,使用迭代器设计模式实现了IEnumerable和IEnumerator,返回的是枚举器。今天我们也来依葫芦画瓢。首先来看看这两个接口的定义:

并没有想象的那么复杂。其中IEnumerable只有一个返回IEnumerator的GetEnumerator方法。而IEnumerator中有两个方法加一个属性。接下来开发画瓢,我们继承IEnumerable接口并实现:

下面使用原始的方式调用:

有朋友开始说了,我们平时都是通过foreache来取值的,没有这样使用过啊。好吧,我们来使用foreach循环:

为什么说基本上是等效的呢?我们先看打印结果,在看反编译代码。

由此可见,两者有这么个关系:

现在我们可以回答为什么在foreach中不能修改item的值?

我们还记得IEnumerator的定义吗

 

接口的定义就只有get没有set。所以我们在foreach中不能修改item的值。

我们再来回答另一个问题:“要实现foreach需要满足什么条件?”:

必须实现IEnumerable接口?NO

我们自己写的MyIEnumerable删掉后面的IEnumerable接口一样可以foreach(不信?自己去测试)。

所以要可以foreach只需要对象定义了GetEnumerator无参方法,并且返回值是IEnumerator或其对应的泛型。细看下图:

也就是说,只要可以满足这三步调用即可。不一定要继承于IEnumerable。有意思吧!下次面试官问你的时候一定要争个死去活来啊,哈哈!

2、yield的使用

你肯定发现了我们自己去实现IEnumerator接口还是有些许麻烦,并且上面的代码肯定是不够健壮。对的,.net给我们提供了更好的方式。

你会发现我们连MyIEnumerator都没要了,也可以正常运行。太神奇了。yield到底为我们做了什么呢?

好家伙,我们之前写的那一大坨。你一个yield关键字就搞定了。最妙的是这块代码:

这就是所谓的状态机吧!

我们继续来看GetEnumerator的定义和调用:

我们调用GetEnumerator的时候,看似里面for循环了一次,其实这个时候没有做任何操作。只有调用MoveNext的时候才会对应调用for循环:

现在我想可以回答你“为什么Linq to Object中要返回IEnumerable?”:

因为IEnumerable是延迟加载的,每次访问的时候才取值。也就是我们在Lambda里面写的where、select并没有循环遍历(只是在组装条件),只有在ToList或foreache的时候才真正去集合取值了。这样大大提高了性能。

如:

这个时候得到了就是IEnumerable对象,但是没有去任何遍历的操作。(对照上面的gif动图看)

什么,你还是不信?那我们再来做个实验,自己实现MyWhere:

现在看到了吧。执行到MyWhere的时候什么动作都没有(返回的就是IEnumerable),只有执行到ToList的时候才代码才真正的去遍历筛选。

这里的MyWhere其实可以用扩展方法来实现,提升逼格。(Linq的那些查询操作符就是以扩展的形式实现的)

3、怎样高性能的随机取IEnumerable中的值

 

 

三、IEnumrator接口

IEnumrator接口包含了3个函数成员:Current、MoveNext以及Reset;

.Current是返回序列中当前位置项的属性。(注意:Current它是只读属性,它返回Object类型的引用,所以可以返回任意类型)

.MoveNext是把枚举器位置前进到集合中下一项的方法。它也但会布尔值,指示新的位置是否是有效位置。

注:如果返回的位置是有效的,方法返回true;

  如果新的位置是无效的,方法返回false;

  枚举器的原始位置在序列中的第一项之前,因此MoveNext必须在第一次使用Current之前调用。

.Reset是把位置重置为原始状态的方法。

 下面我们用图表示一下他们之间的关系

 

有了集合的枚举器,我们就可以使用MoveNext和Current成员来模仿foreach循环遍历集合中的项,例如,我们已经知道数组是可枚举类型,所以下面的代码手动做foreach语句

自动做的事情。

代码如下:

复制代码
 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Threading.Tasks;
 6 using System.Collections;
 7 
 8 namespace ConsoleApplication1
 9 {
10     class Program
11     {
12         static void Main(string[] args)
13         {
14             int[] arr = { 1, 2, 3, 4, 5, 6 };
15             IEnumerator ie = arr.GetEnumerator();
16             while( ie.MoveNext() )
17             {
18                 int i = (int)ie.Current;
19                 Console.WriteLine("{0}", i);
20             }
21         }
22     }
23 }
复制代码

程序运行的结果为

我们来用图解释一下代码中的数组结构

IEnumerable接口

数组是可枚举类型,是因为实现了IEnumerable接口的类,所以可枚举类都是因为实现了IEnumerable接口的类。

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

如图所示:

下面我们举一个使用IEnumerator和IEnumerable的例子

下面的代码展示了一个可枚举类的完整示例,该类叫Component(球形)。它的枚举器类为Shape(形状)。

代码如下:

复制代码
 1 using System;
 2 using System.Collections;
 3 
 4 namespace ConsoleApplication1
 5 {
 6     class Shape : IEnumerator
 7     {
 8         string[] _Shapes;
 9         int _Position = -1;
10 
11         public Shape(string[] _theShapes)
12         {
13             _Shapes = new string[_theShapes.Length];
14             for( int i = 0; i < _theShapes.Length; i++ )
15             {
16                 _Shapes[i] = _theShapes[i];
17             }
18         }
19 
20         public Object Current
21         {
22             get
23             {
24                 if ( _Position == -1 )
25                     throw new InvalidOperationException();
26                 if (_Position >= _Shapes.Length)
27                     throw new InvalidOperationException();
28                 return _Shapes[_Position];
29             }
30         }
31 
32         public bool MoveNext()
33         {
34             if (_Position < _Shapes.Length - 1)
35             {
36                 _Position++;
37                 return true;
38             }
39             else
40                 return false;
41         }
42 
43         public void Reset()
44         {
45             _Position = -1;
46         }
47     }
48 
49     class Component : IEnumerable
50     {
51         string[] shapes = { "Circular", "spherical", "Quadrilateral", "Label" };
52         public IEnumerator GetEnumerator()
53         {
54             return new Shape( shapes );
55         }
56     }
57 
58     class Program
59     {
60         static void Main(string[] args)
61         {
62             Component comp = new Component();
63             foreach ( string oshape in comp )
64                 Console.WriteLine(oshape);
65         }
66        
67     }
68 }
复制代码

运行结果:

 

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

https://www.cnblogs.com/qtiger/p/13571909.html

原文地址:https://www.cnblogs.com/qtiger/p/13609206.html