zoukankan      html  css  js  c++  java
  • 【基础】迭代器详解

    一、前言

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

    二、提出问题

    foreach背后的原理是什么?

    foreach循环中为什么只能读数据,不能修改数据?

    如果想实现foreach遍历,必须要实现IEnumberable接口么?

    可以自己实现在foreach中修改数据么?

    三、自己实现迭代器

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

     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‰

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

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

     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在迭代过程中,可以修改迭代变量的属性但不可以修改迭代变量本身;而当迭代变量为值类型的时候,既不可以修改迭代变量本身也不可以修改迭代变量的属性(如果存在)。

    四、总结

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

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

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

    第三、在foreach循环中,不能修改值类型的数据,包括结构体的属性等,也不能修改引用类型数据本身,但是却可以修改类的属性。

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

    作者:悠扬的牧笛

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

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

  • 相关阅读:
    SecureCRT 安装及初始化配置
    企业生产环境中linux系统分区的几种方案
    Django之验证码 + session 认证
    Django之上传文件
    Django之Cookie与Session
    Django之CSRF 跨站请求伪造
    web前端之 DOM
    c++ 之 字符和字符串
    web前端
    调用线程无法访问此对象,因为另一个线程拥有该对象
  • 原文地址:https://www.cnblogs.com/xhb-bky-blog/p/6369882.html
Copyright © 2011-2022 走看看