你的位置:首页 > ASP.net教程

[ASP.net教程]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.113  IL_003d: ldloc.114  IL_003e: ldloc.015  IL_003f: ldlen16  IL_0040: conv.i417  IL_0041: clt18  IL_0043: stloc.s  CS$4$000119  IL_0045: ldloc.s  CS$4$000120  IL_0047: brtrue.s  IL_002e  //跳转到第2行21  IL_0049: nop22  IL_004a: ldloc.023  IL_004b: stloc.s  CS$6$000224  IL_004d: ldc.i4.025  IL_004e: stloc.s  CS$7$000326  IL_0050: br.s    IL_0067  //foreach开始的地方27  IL_0052: ldloc.s  CS$6$000228  IL_0054: ldloc.s  CS$7$000329  IL_0056: ldelem.ref30  IL_0057: stloc.231  IL_0058: nop32  IL_0059: ldloc.233  IL_005a: call    void [mscorlib]System.Console::WriteLine(string)34  IL_005f: nop35  IL_0060: nop36  IL_0061: ldloc.s  CS$7$0003 //37  IL_0063: ldc.i4.1       //38  IL_0064: add          //当前索引处加139  IL_0065: stloc.s  CS$7$000340  IL_0067: ldloc.s  CS$7$000341  IL_0069: ldloc.s  CS$6$000242  IL_006b: ldlen43  IL_006c: conv.i444  IL_006d: clt45  IL_006f: stloc.s  CS$4$000146  IL_0071: ldloc.s  CS$4$000147  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;      }    }  }