【基础】迭代器详解

1、前言

在咱们的平常工做中,使用foreach循环对集合进行迭代操做,是最经常使用的操做之一。有时咱们会遇到这样的需求,在遍历迭代元素集合的过程当中,根据需求去筛选修改元素,因而就顺手使用foreach进行迭代并修改,固然编译的时候会报错,提示咱们在迭代的过程重视不容许对元素进行修改的,此时咱们关心的是业务逻辑而并不是代码自己,因而咱们掉头寻找其余的解决方案。下面咱们就来看看foreach迭代器的工做过程。html

2、提出问题

foreach背后的原理是什么?数组

foreach循环中为何只能读数据,不能修改数据?函数

若是想实现foreach遍历,必需要实现IEnumberable接口么?测试

能够本身实如今foreach中修改数据么?ui

3、本身实现迭代器

首先经过反编译来看一下迭代器代码:spa

 1 namespace System.Collections.Generic
 2 {
 3     using System.Collections;
 4     using System.Runtime.CompilerServices;
 5     
 6     [TypeDependency("System.SZArrayHelper"), __DynamicallyInvokable]
 7     public interface IEnumerable<out T> : IEnumerable
 8     {
 9         [__DynamicallyInvokable]
10         IEnumerator<T> GetEnumerator();
11     }
12 }
IEnumerable接口很简单,只包含了一个返回类型为IEnumerator的GetEnumerator方法。
 1 namespace System.Collections
 2 {
 3     using System;
 4     using System.Runtime.InteropServices;
 5     
 6     [Guid("496B0ABF-CDEE-11d3-88E8-00902754C43A"), ComVisible(true), __DynamicallyInvokable]
 7     public interface IEnumerator
 8     {
 9         [__DynamicallyInvokable]
10         bool MoveNext();                                //将游标的内部位置向前移动
11         [__DynamicallyInvokable]
12         object Current { [__DynamicallyInvokable] get; }//获取当前的项(只读属性)
13         [__DynamicallyInvokable]
14         void Reset();                                   //将游标重置到第一个成员前面
15     }
16 }

IEnumberator接口包含了两个方法和一个只读属性,MoveNext方法返回值为bool类型,若是指针移动到下一个索引位置有效则返回True,不然返回False;Reset方法用于将游标重置到第一个成员前面;Current属性用于读取当前索引项(只读)。代码中我手动添加了注释。既然获得了反编译后的代码接口声明,那咱们就模仿着写一个相同功能的接口来实现本身的迭代器。指针

IEnumerator接口包含三个函数成员: CurrentMoveNext以及Reset‰ code

  • Current返回序列中当前位置项的属性;它是只读属性;它返回object类型的引用,因此能够返回任何类型。
  • ‰MoveNext是把枚举数位置前进到集合中下一项的方法。它也返回布尔值,指示新的位置是有效位置或已经超过了序列的尾部。若是新的位置是有效的,方法返回true若是新的位置是无效的(好比到达了尾部),方法返回false枚举数的原始位置在序列中的第一项以前。MoveNext必须在第一次使用Current以前使用,不然CLR会抛出一个InvalidOperationException异常。
  • ‰Reset方法把位置重置为原始状态。

如下代码和反编译出来的代码几乎是如出一辙的,代码以下:htm

 1 namespace Xhb.IEnumberable
 2 {
 3     public interface IEnumerable
 4     {
 5         IEnumerator GetEnumerator();
 6     }
 7 
 8     public interface IEnumerator
 9     {
10         object Current { get; } //获取当前的项(只读属性)
11         bool MoveNext();        //将游标的内部位置向前移动
12         void Reset();           //将游标重置到第一个成员前面
13     }
14 }                              

下面咱们来本身实现具体的迭代器功能,新增一个UserEnumerable 类并实现IEnumerable接口,同时新增一个UserEnumerator类来实现IEnumerator接口,编写代码逻辑以下:对象

 1 namespace Xhb.IEnumberable
 2 {
 3     class UserEnumerable : Xhb.IEnumberable.IEnumerable
 4     {
 5         private string[] _info;
 6 
 7         public UserEnumerable(string[] info)
 8         {
 9             _info = info;
10         }
11 
12         public IEnumerator GetEnumerator()
13         {
14             return new UserEnumerator(_info); //返回一个实现了IEnumerator接口的实例
15         }
16     }
17 }
 1 namespace Xhb.IEnumberable
 2 {
 3     /// <summary>
 4     /// 自定义迭代器
 5     /// </summary>
 6     class UserEnumerator : Xhb.IEnumberable.IEnumerator
 7     {
 8 
 9         private string[] _info; 
10         private int position;   //存放当前指针位置信息
11         public UserEnumerator(string[] info)
12         {
13             _info = info;
14             position = -1;      //初始化位置信息
15         }
16         public object Current 17         {
18             get
19             {
20                 return _info[position]; //返回当前指针指向的元素
21             }
22         }
23 
24         public bool MoveNext() 25         {
26             position++;
27             return (position < _info.Length) ? true : false;
28         }
29 
30         public void Reset() 31         {
32             position = -1; //复位指针位置
33         }
34     }
35 }

这样咱们就实现了本身的迭代器,下图说明了可枚举类型和枚举数之间的关系

下面咱们来测试一下效果,在Main方法中编写以下代码进行测试:

 1 namespace Xhb.IEnumberable
 2 {
 3     class Program
 4     {
 5         static void Main(string[] args)
 6         {
 7             //定义数据源
 8             string[] info =
 9             {
10                 "两个黄鹂鸣翠柳,",
11                 "一行白鹭上青天。",
12                 "窗含西岭千秋雪,",
13                 "门泊东吴万里船。"
14             };
15 
16             //以原始的方式调用
17             UserEnumerable userEnum = new UserEnumerable(info);
18             //获取实现了IEnumerable接口的实例
19             var instance = userEnum.GetEnumerator();
20             //开始遍历输出
21             while (instance.MoveNext())
22             {
23                 Console.WriteLine(instance.Current);
24             }
25             Console.ReadLine();
26         }
27     }
28 }

输出结果就不在这里展现了,就是我在代码中定义的info私有变量。这段代码的运行过程是这样的,首先在UserEnumerable的构造函数中,传入了一个string类型的数组做为数据源,UserEnumerable是实现了IEnumerable接口的,也就实现了IEnumerable接口中的GetEnumerator方法,该方法返回了一个将传入的数据源做为参数而且实现了IEnumerator接口的UserEnumerator实例。这样在UserEnumerator类中就能够经过实现的IEnumerator接口的成员对数据源进行遍历操做了。其实,这段代码和foreach进行遍历的效果是如出一辙的。那么若是不实现IEnumerable接口可不可使用foreach进行遍历呢?下面添加一个NonUserEnumerable类来进行下验证,代码以下:

 1 namespace Xhb.IEnumberable
 2 {
 3     class NonUserEnumerable
 4     {
 5         private string[] _info;
 6 
 7         public NonUserEnumerable(string[] info)
 8         {
 9             _info = info;
10         }
11 
12         public IEnumerator GetEnumerator()
13         {
14             return new UserEnumerator(_info); //返回一个实现了IEnumerator接口的实例
15         }
16     }
17 }

其实很简单,就是在UserEnumerable类的基础上把实现IEnumerable接口的部分删掉了,通过测试发现,竟然能够foreach遍历,因此实现IEnumerable接口不是foreach遍历的必要条件,可是须要定义和IEnumerable接口同样的成员,即存在GetEnumerator无参方法,而且返回值是IEnumerator或其对应的泛型便可。yield 关键字向编译器指示它所在的方法是迭代器块。编译器生成一个类来实现迭代器块中表示的行为。在迭代器块中,yield 关键字与 return 关键字结合使用,向枚举器对象提供值。这是一个返回值,例如,在 foreach 语句的每一次循环中返回的值。yield 关键字也可与 break 结合使用,表示迭代结束。

还有一个问题,在迭代的过程当中,是否能够修改当前索引的值呢?咱们在开发的过程当中不少的时候都会遇到这种场景,就是对于一个集合中全部元素进行过滤修改,若是符合修改条件就进行更改,可是咱们的作法一般是使用for循环,或者其余的方式,下面咱们在这个小例子中实如今迭代中也能修改元素的功能。

 1 namespace Xhb.IEnumberable
 2 {
 3     /// <summary>
 4     /// 自定义迭代器
 5     /// </summary>
 6     class UserEnumerator : Xhb.IEnumberable.IEnumerator
 7     {
 8 
 9         private string[] _info; 
10         private int position;   //存放当前指针位置信息
11         public UserEnumerator(string[] info)
12         {
13             _info = info;
14             position = -1;      //初始化位置信息
15         }
16         public object Current
17         {
18             get
19             {
20                 return _info[position]; //返回当前指针指向的元素
21             }
22             set
23  { 24                 //为Current属性添加可写访问
25                 _info[position]=value.ToString(); 26  } 27         }
28 
29         public bool MoveNext()
30         {
31             position++;
32             return (position < _info.Length) ? true : false;
33         }
34 
35         public void Reset()
36         {
37             position = -1; //复位指针位置
38         }
39     }
40 }

注意上面代码中加粗倾斜的部分,就是为Current属性添加了set访问器,下面来看一下调用方代码:

 1 namespace Xhb.IEnumberable
 2 {
 3     class Program
 4     {
 5         static void Main(string[] args)
 6         {
 7             //定义数据源
 8             string[] info =
 9             {
10                 "两个黄鹂鸣翠柳,",
11                 "一行白鹭上青天。",
12                 "窗含西岭千秋雪,",
13                 "门泊东吴万里船。"
14             };
15 
16             //以原始的方式调用
17             //UserEnumerable userEnum = new UserEnumerable(info);
18             UserEnumerable userEnum = new UserEnumerable(info);
19             //获取实现了IEnumerable接口的实例
20             var instance = userEnum.GetEnumerator();
21             //开始遍历输出
22             while (instance.MoveNext()) 23  { 24 instance.Current = instance.Current + "<"; //为Current属性赋值 25  Console.WriteLine(instance.Current); 26  } 27 
28             Console.WriteLine("--------------------------");
29 
30             foreach (var item in userEnum) 31  { 32 item = "New Value"; //报错信息 : 没法为“item”赋值,由于它是“foreach迭代变量” 33  Console.WriteLine(item); 34  } 35             Console.ReadLine();
36         }
37     }
38 }

上述代码中,一样重点关注加粗倾斜部分的代码,在while循环中,我为Current属性赋值后再输出。注意,在前面的代码中这是不被容许的,由于Current属性是只读的。而我在自定义迭代器中为Current添加了set访问器后,就能够在遍历时修改元素的值。再来看上述代码的foreach循环,即使我给Current属性添加了set访问器,仍然不能修改item的值,报错信息我加在了注释中。那么,是否是能够得出这样的结论?不管迭代对象的Current属性是否是可写,在foreach中item都是不容许被赋值的。咱们姑且去验证一下。在这个例子中,我采用的是string类型的数组,下面我使用struct集合和class集合来分别做为迭代的数据源进行测试。

首先使用struct数组做为测试迭代的数据源,代码以下:

 1 class Program
 2 {
 3     static void Main(string[] args)
 4     {
 5         
 6         //类集合做为数据源
 7         StructPoint[] structPoint = new StructPoint[]
 8         {
 9             new StructPoint() {X=30,Y=63 },
10             new StructPoint() {X=34,Y=65 },
11             new StructPoint() {X=38,Y=68 }
12         };
13 
14         //用于测试赋值操做
15         StructPoint sp = new StructPoint() { X = 12, Y = 25 };
16 
17         //以原始的方式调用
18         UserEnumerable userEnum = new UserEnumerable(structPoint);
19         
20         //获取实现了IEnumerable接口的实例
21         var instance = userEnum.GetEnumerator();
22         
23         //开始遍历输出
24         while (instance.MoveNext())
25         {
26             instance.Current = sp;
27             StructPoint tmp = (StructPoint)instance.Current;
28             Console.WriteLine(tmp.X);
29         }
30 
31         Console.WriteLine("--------------------------");
32 
33         foreach (StructPoint item in userEnum) 34  { 35 item =sp; //报错信息 : 没法为“item”赋值,由于它是“foreach迭代变量” 36 item.Y = sp.Y; //报错信息 : “item”是一个“foreach迭代变量”,所以没法修改其成员 37  Console.WriteLine(item.Y); 38  } 39         Console.ReadLine();
40     }
41 }

由上面的代码能够看出,在对struct数组进行迭代的时候,不管是修改item自己仍是修改item的成员,都是不被容许的,具体的错误信息我已经在注释中标注了。下面来看下采用class的数组做为数据源的时候,会发生什么,代码以下:

 1 class Program
 2 {
 3     static void Main(string[] args)
 4     {
 5         
 6         //类集合做为数据源
 7         ClassPoint[] classPoint = new ClassPoint[]
 8         {
 9             new ClassPoint() {X=30,Y=63 },
10             new ClassPoint() {X=34,Y=65 },
11             new ClassPoint() {X=38,Y=68 }
12         };
13 
14         //用于测试赋值操做
15         ClassPoint cp = new ClassPoint() { X = 12, Y = 2 };
16 
17         //以原始的方式调用
18         UserEnumerable userEnum = new UserEnumerable(classPoint);
19         
20         //获取实现了IEnumerable接口的实例
21         var instance = userEnum.GetEnumerator();
22         
23         //开始遍历输出
24         while (instance.MoveNext())
25         {
26             instance.Current = cp;
27             ClassPoint tmp = (ClassPoint)instance.Current;
28             Console.WriteLine(tmp.X);
29         }
30 
31         Console.WriteLine("--------------------------");
32 
33         foreach (ClassPoint item in userEnum) 34  { 35 item =cp; //报错信息 : 没法为“item”赋值,由于它是“foreach迭代变量” 36 item.Y = cp.Y; //这里已经不报错了!!! 37  Console.WriteLine(item.Y); 38  } 39         Console.ReadLine();
40     }
41 }

一样地,当使用class数组做为迭代数据源时,在迭代的过程当中,item自己是不容许被修改的,可是item的成员倒是容许被修改并且不会报错!具体的过程我一样在注释中标明了。经过以上代码的运行对比,咱们不难发现一个规律:当迭代变量为引用类型的时候,foreach在迭代过程当中,能够修改迭代变量的属性但不能够修改迭代变量自己;而当迭代变量为值类型的时候,既不能够修改迭代变量自己也不能够修改迭代变量的属性(若是存在)。

4、总结

通过上面的叙述以及代码演示,如今咱们再回过头来看一下第二节中提出的问题,针对问题进行以下的总结:

第1、若是想使用foreach进行迭代,那么迭代的对象必须存在GetEnumerator方法返回IEnumerator接口实例

第2、由于Current属性是只读的,因此在进行foreach迭代的时候不能够修改item的值(某些资料上是这么说的,但我不认同,在上面的代码中我已经为Current属性添加了set访问器,在while循环的时候是能够修改被迭代对象的值)。

第3、在foreach循环中,不能修改值类型的数据,包括结构体的属性等,也不能修改引用类型数据自己,可是却能够修改类的属性。

每个小的知识点展开后,后面都有不少很是有意思且值得咱们去深刻探究的东西,本文就算是回顾基础吧,若是文中有表述不稳当的地方,请及时评论或私信,我会及时更正,欢迎共同交流讨论。

做者:悠扬的牧笛

博客地址:http://www.cnblogs.com/xhb-bky-blog/p/6369882.html

声明:本博客原创文字只表明本人工做中在某一时间内总结的观点或结论,与本人所在单位没有直接利益关系。非商业,未受权贴子请以现状保留,转载时必须保留此段声明,且在文章页面明显位置给出原文链接。

相关文章
相关标签/搜索