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