zoukankan      html  css  js  c++  java
  • C# IEnumerable、IEnumerator和foreach的联系与解析

    1、关于foreach和for

    foreach和for都是循环的关键字,使用这两个关键字可以对集合对象进行遍历,获取里面每一个对象的信息进行操作。

      static void Main(string[] args)
            {
                string[] strList = new string[]
                {
                    "1","2","3","4"
                };
    
                for (int i = 0; i < strList.Length; i++)
                {
                    Console.WriteLine(strList[i]);
                }
    
                foreach (string str in strList)
                {
                    Console.WriteLine(str);
                }
    
                Console.ReadKey();
            }

    上面结果的输出都是一样的,我们来看看IL是否是一样的。

     1 IL_002c:  br.s       IL_003d   //for开始的地方
     2   IL_002e:  nop
     3   IL_002f:  ldloc.0
     4   IL_0030:  ldloc.1
     5   IL_0031:  ldelem.ref
     6   IL_0032:  call       void [mscorlib]System.Console::WriteLine(string)
     7   IL_0037:  nop
     8   IL_0038:  nop
     9   IL_0039:  ldloc.1  //
    10   IL_003a:  ldc.i4.1 //
    11   IL_003b:  add      //索引加1,这里的索引是已经保存在堆栈中的索引
    12   IL_003c:  stloc.1
    13   IL_003d:  ldloc.1
    14   IL_003e:  ldloc.0
    15   IL_003f:  ldlen
    16   IL_0040:  conv.i4
    17   IL_0041:  clt
    18   IL_0043:  stloc.s    CS$4$0001
    19   IL_0045:  ldloc.s    CS$4$0001
    20   IL_0047:  brtrue.s   IL_002e   //跳转到第2行
    21   IL_0049:  nop
    22   IL_004a:  ldloc.0
    23   IL_004b:  stloc.s    CS$6$0002
    24   IL_004d:  ldc.i4.0
    25   IL_004e:  stloc.s    CS$7$0003
    26   IL_0050:  br.s       IL_0067   //foreach开始的地方
    27   IL_0052:  ldloc.s    CS$6$0002
    28   IL_0054:  ldloc.s    CS$7$0003
    29   IL_0056:  ldelem.ref
    30   IL_0057:  stloc.2
    31   IL_0058:  nop
    32   IL_0059:  ldloc.2
    33   IL_005a:  call       void [mscorlib]System.Console::WriteLine(string)
    34   IL_005f:  nop
    35   IL_0060:  nop
    36   IL_0061:  ldloc.s    CS$7$0003  //
    37   IL_0063:  ldc.i4.1              //
    38   IL_0064:  add                   //当前索引处加1
    39   IL_0065:  stloc.s    CS$7$0003
    40   IL_0067:  ldloc.s    CS$7$0003
    41   IL_0069:  ldloc.s    CS$6$0002
    42   IL_006b:  ldlen
    43   IL_006c:  conv.i4
    44   IL_006d:  clt
    45   IL_006f:  stloc.s    CS$4$0001
    46   IL_0071:  ldloc.s    CS$4$0001
    47   IL_0073:  brtrue.s   IL_0052     //跳转到27行

    从IL可以看出,for中循环的索引是for自身的索引(即i),foreach在循环过程中会在指定位置存储一个值,这个值就是循环用的索引。所以,其实foreach内部还是存储了一个索引值用于循环,只是我们在用的过程中没有察觉到存在这个变量而已。

    我们再来看看下面这个例子:

     static void RunFor()
            {
                string[] strList = new string[]
                {
                    "1","2","3","4"
                };
    
                for (int i = 0; i < strList.Length; i++)
                {
                    strList[i] = "1";
                }
            }
    
            static void RunForeach()
            {
                string[] strList = new string[]
                {
                    "1","2","3","4"
                };
    
                foreach (string str in strList)
                {
                    str = "1";
                }
            }

    编译出错 : “str”是一个“foreach 迭代变量”,无法为它赋值

     static void RunFor()
            {
                List<string> strList = new List<string>()
                {
                    "1","2","3","4"
                };
    
                for (int i = 0; i < strList.Count; i++)
                {
                    strList[i] = "1";
                }
            }
    
            static void RunForeach()
            {
                List<string> strList = new List<string>()
                {
                    "1","2","3","4"
                };
    
                foreach (string str in strList)
                {
                    str = "1";
                }
            }

    同样,编译器给出了相同的错误。

    那么如果在foreach中移除当前项呢?

    class Program
        {
            static void Main(string[] args)
            {
                List<string> strs = new List<string>() { "1", "2", "3", "4" };
                foreach (string str in strs)
                {
                    strs.Remove(str);
                }
                Console.ReadKey();
            }
        }

    运行出现了异常

    可以看出移除IEnumerable类型的变量也会出错,所以在foreach中是不能改变进行迭代的集合对象值的。

    2、foreach和IEnumerable的联系

    像List,Array等集合类型,可以使用for和foreach来对其进行循环迭代,获得每一个集合内的对象用于操作。之所以可以使用foreach,是因为List,Array等类型实现了IEnumerable或者IEnumerable<T>接口。

    public interface IEnumerable
    {
        IEnumerator GetEnumerator();
    }

    IEnumerable接口内部只有一个方法,GetEnumerator()方法,返回值是一个IEnumerator类型的对象。

    public interface IEnumerator
    {
        bool MoveNext();
        object Current { get; }
        void Reset();
    }

    可以看出,在IEnumerator接口中有三个成员,用于移动位置的MoveNext函数,表示当前对象的Current属性,重置函数Reset。

     我们以ArrayList类型为例,来看看这个接口是怎么实现的。

    首先内部有一个数组变量用于存储遍历的集合对象。

    object[] _items;

    在内部私有的类ArrayListEnumeratorSimple中实现了IEnumerator接口成员。

     1  public bool MoveNext()
     2     {
     3         int num;
     4         if (this.version != this.list._version)
     5         {
     6             throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_EnumFailedVersion"));
     7         }
     8         if (this.isArrayList)
     9         {
    10             if (this.index < (this.list._size - 1))
    11             {
    12                 num = this.index + 1;
    13                 this.index = num;
    14                 this.currentElement = this.list._items[num]; //其实还是取得内部的数组变量的成员
    15                 return true;
    16             }
    17             this.currentElement = dummyObject;
    18             this.index = this.list._size;
    19             return false;
    20         }
    21         if (this.index < (this.list.Count - 1))
    22         {
    23             num = this.index + 1;
    24             this.index = num;
    25             this.currentElement = this.list[num]; //数组变量的成员
    26             return true;
    27         }
    28         this.index = this.list.Count;
    29         this.currentElement = dummyObject;
    30         return false;
    31     }

     在MoveNext中进行迭代循环的时候迭代的是内部的_items数组,即每次取的值都是_items的成员,而_items数组是ArrayList的索引数组。每次迭代后都会保存当前索引值用于下次使用。

     所以不难看出,IEnumerator接口内部实现的方式归根结底还是和for实现的方式一样的。

    之所以修改枚举值过后继续访问会抛出InvalidOperationException异常是因为以下代码:

     if (this.version != this.list._version)
      {
         throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_EnumFailedVersion"));
      }

    在Reset和MoveNext中都有这个判断,如果枚举值被修改了,他所对应的版本号将会发生改变(在Remove函数中将会执行this._version++,使得版本号发生了改变,其他改变枚举值状态的函数类似)。

    3、自定义实现迭代器

     具体实现代码:

     class Program
        {
            static void Main(string[] args)
            {
                TestIEnumerable test = new TestIEnumerable();
                foreach (string str in test)
                {
                    Console.WriteLine(str);
                }
                Console.ReadKey();
            }
        }
    
        class TestIEnumerable : IEnumerable
        {
            private string[] _item;
    
            public TestIEnumerable()
            {
                _item = new string[]
                 {
                     "1","2","3","4"
                 };
            }
    
            public string this[int index]
            {
                get { return _item[index]; }
            }
    
            public IEnumerator GetEnumerator()
            {
                return new EnumeratorActualize(this);
            }
    
            class EnumeratorActualize : IEnumerator
            {
                private int index;
                private TestIEnumerable _testEnumerable;
                private object currentObj;
                public EnumeratorActualize(TestIEnumerable testEnumerable)
                {
                    _testEnumerable = testEnumerable;
                    currentObj = new object();
                    index = -1;
                }
    
    
                public object Current
                {
                    get
                    {
                        return currentObj;
                    }
                }
    
                public bool MoveNext()
                {
                    if (index < _testEnumerable._item.Length - 1)
                    {
                        index++;
                        currentObj = _testEnumerable._item[index];
                        return true;
                    }
                    index = _testEnumerable._item.Length;
                    return false;
                }
    
                public void Reset()
                {
                    index = -1;
                }
            }
        }
  • 相关阅读:
    xgqfrms™, xgqfrms® : xgqfrms's offical website of GitHub!
    xgqfrms™, xgqfrms® : xgqfrms's offical website of GitHub!
    xgqfrms™, xgqfrms® : xgqfrms's offical website of GitHub!
    【手绘漫画】图解LeetCode之最长上升子序列(LeetCode300题),贪心算法 + 二分查找
    C 语言编程 — GDB 调试工具
    【debug】Matplotlib is currently using agg, which is a non-GUI backend, so cannot show the figure.
    tf.expand_dims()函数解析(最清晰的解释)
    C 语言编程 — 堆栈与内存管理
    C 语言编程 — 输入/输出与文件操作
    C 语言编程 — 头文件
  • 原文地址:https://www.cnblogs.com/qingsp/p/5603984.html
Copyright © 2011-2022 走看看