在咱们的平常工做中,使用foreach循环对集合进行迭代操做,是最经常使用的操做之一。有时咱们会遇到这样的需求,在遍历迭代元素集合的过程当中,根据需求去筛选修改元素,因而就顺手使用foreach进行迭代并修改,固然编译的时候会报错,提示咱们在迭代的过程重视不容许对元素进行修改的,此时咱们关心的是业务逻辑而并不是代码自己,因而咱们掉头寻找其余的解决方案。下面咱们就来看看foreach迭代器的工做过程。html
foreach背后的原理是什么?数组
foreach循环中为何只能读数据,不能修改数据?函数
若是想实现foreach遍历,必需要实现IEnumberable接口么?测试
能够本身实如今foreach中修改数据么?ui
首先经过反编译来看一下迭代器代码: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接口包含三个函数成员: Current、 MoveNext以及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在迭代过程当中,能够修改迭代变量的属性但不能够修改迭代变量自己;而当迭代变量为值类型的时候,既不能够修改迭代变量自己也不能够修改迭代变量的属性(若是存在)。
通过上面的叙述以及代码演示,如今咱们再回过头来看一下第二节中提出的问题,针对问题进行以下的总结:
第1、若是想使用foreach进行迭代,那么迭代的对象必须存在GetEnumerator方法返回IEnumerator接口实例
第2、由于Current属性是只读的,因此在进行foreach迭代的时候不能够修改item的值(某些资料上是这么说的,但我不认同,在上面的代码中我已经为Current属性添加了set访问器,在while循环的时候是能够修改被迭代对象的值)。
第3、在foreach循环中,不能修改值类型的数据,包括结构体的属性等,也不能修改引用类型数据自己,可是却能够修改类的属性。
每个小的知识点展开后,后面都有不少很是有意思且值得咱们去深刻探究的东西,本文就算是回顾基础吧,若是文中有表述不稳当的地方,请及时评论或私信,我会及时更正,欢迎共同交流讨论。
做者:悠扬的牧笛
博客地址:http://www.cnblogs.com/xhb-bky-blog/p/6369882.html
声明:本博客原创文字只表明本人工做中在某一时间内总结的观点或结论,与本人所在单位没有直接利益关系。非商业,未受权贴子请以现状保留,转载时必须保留此段声明,且在文章页面明显位置给出原文链接。