开始之前先思考几个问题:
- 为什么集合可以使用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(); } } }
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 }
运行结果: