【基礎】迭代器詳解

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

聲明:本博客原創文字只表明本人工做中在某一時間內總結的觀點或結論,與本人所在單位沒有直接利益關係。非商業,未受權貼子請以現狀保留,轉載時必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。

相關文章
相關標籤/搜索