迭代器模式隨想
- 2019 年 10 月 3 日
- 筆記
一、什麼是迭代器模式?
定義:提供一種順序訪問集合的方法,而不暴露集合內部的表示
順序訪問,one by one(挨個訪問),不暴露集合內部表示,反映了面向對象程式中的封裝性。可以這麼理解,一組模特從後台出場,一個接著一個,但是先出場的模特,未必是站在最前面的模特。換句話說,對於觀眾,你不知道後台模特的特定位置。為什麼是順序訪問呢?因為迭代器模式採用的輸出機制是內部決定好的,你無法決定。不像字典類型,我傳不同的key,可以訪問不同的value。我們訪問列表,可以直接訪問第i個元素,但是迭代器,你想要訪問下一個元素,必須把當前的元素訪問過後,才能到下一個元素。
二、c#中的迭代器介面
迭代器介面,可以手動實現調用,如下:
1 public class MyIEnumerator : IEnumerator<string> 2 { 3 string[] types = { "下等馬", "上等馬", "中等馬" }; 4 5 int cur = -1; 6 public string Current 7 { 8 get 9 { 10 return types[cur]; 11 } 12 } 13 object IEnumerator.Current 14 { 15 get 16 { 17 return this.Current; 18 } 19 } 20 21 public void Dispose() 22 { 23 } 24 25 public bool MoveNext() 26 { 27 if (cur < types.Length - 1) 28 { 29 cur++; 30 return true; 31 } 32 return false; 33 } 34 35 public void Reset() 36 { 37 cur = -1; 38 } 39 }
話說田忌賽馬,按一定的出場順序贏得了齊威王,此策略是由孫臏提出的,再看看調用:
1 MyIEnumerator m = new MyIEnumerator(); 2 while (true) 3 { 4 if (m.MoveNext()) { Console.WriteLine(m.Current); } 5 else break; 6 }
顯然手動編寫程式碼,比較麻煩,搞不好還弄個數組越界,我們看看c#中可枚舉介面:
此介面只有一個實現迭代器的方法,我們知道凡是實現了這個介面的,都可以用foreach循環,我們把調用迭代器的方法改成foreach自動調用
1 public class MyEnumerable : IEnumerable<string> 2 { 3 public IEnumerator<string> GetEnumerator() 4 { 5 return new MyIEnumerator(); 6 } 7 8 IEnumerator IEnumerable.GetEnumerator() 9 { 10 return GetEnumerator(); 11 } 12 }
1 MyEnumerable a = new MyEnumerable(); 2 foreach (var item in a) 3 { 4 Console.WriteLine(item); 5 }
可能有人問了,你改成foreach循環和手動循環調用有什麼區別?可以這麼說吧,foreach進一步簡化了調用,你不用控制循環什麼時候結束,你也不要操心怎麼訪問下一個元素。
可見迭代器的調用已經很優雅了,如果迭代器的創建能夠簡化,那麼就更好了,c#中提供了yield關鍵字。
1 public IEnumerator<string> GetEnumerator() 2 { 3 yield return "下等馬"; 4 yield return "上等馬"; 5 yield return "中等馬"; 6 }
通過6行程式碼,編譯器就為我們創建好了迭代器,如下所示:
1 public class MyEnumerable : IEnumerable<string>, IEnumerable 2 { 3 // Methods 4 [IteratorStateMachine(typeof(<GetEnumerator>d__0))] 5 public IEnumerator<string> GetEnumerator() 6 { 7 yield return "下等馬"; 8 yield return "上等馬"; 9 yield return "中等馬"; 10 } 11 12 IEnumerator IEnumerable.GetEnumerator() 13 { 14 return this.GetEnumerator(); 15 } 16 17 // Nested Types 18 [CompilerGenerated] 19 private sealed class <GetEnumerator>d__0 : IEnumerator<string>, IDisposable, IEnumerator 20 { 21 // Fields 22 private int <>1__state; 23 private string <>2__current; 24 public MyEnumerable <>4__this; 25 26 // Methods 27 [DebuggerHidden] 28 public <GetEnumerator>d__0(int <>1__state) 29 { 30 this.<>1__state = <>1__state; 31 } 32 33 private bool MoveNext() 34 { 35 switch (this.<>1__state) 36 { 37 case 0: 38 this.<>1__state = -1; 39 this.<>2__current = "下等馬"; 40 this.<>1__state = 1; 41 return true; 42 43 case 1: 44 this.<>1__state = -1; 45 this.<>2__current = "上等馬"; 46 this.<>1__state = 2; 47 return true; 48 49 case 2: 50 this.<>1__state = -1; 51 this.<>2__current = "中等馬"; 52 this.<>1__state = 3; 53 return true; 54 55 case 3: 56 this.<>1__state = -1; 57 return false; 58 } 59 return false; 60 } 61 62 [DebuggerHidden] 63 void IEnumerator.Reset() 64 { 65 throw new NotSupportedException(); 66 } 67 68 [DebuggerHidden] 69 void IDisposable.Dispose() 70 { 71 } 72 73 // Properties 74 string IEnumerator<string>.Current 75 { 76 [DebuggerHidden] 77 get 78 { 79 return this.<>2__current; 80 } 81 } 82 83 object IEnumerator.Current 84 { 85 [DebuggerHidden] 86 get 87 { 88 return this.<>2__current; 89 } 90 } 91 } 92 }
有趣的是編譯器用switch case 實現MoveNext方法,yield用狀態機實現迭代器。
三、理解yield關鍵字
yield關鍵字,後面緊跟return 表達式或者break。return 表達式,並沒有結束迭代器,只是暫時離開迭代器,在特定情況下(調用MoveNext方法時),又會進入迭代器。可以理解迭代器處於暫停或者掛起狀態。break直接結束迭代。
四、迭代器的其它用途
Linq表達式的延遲載入,如果我們有一個數組,想通過Linq過濾一下:
List<int> ages =new List<int> { 5, 17, 30, 40 }; var yong = ages.Where(g => g < 18); ages.Add(2); foreach (var item in yong) { Console.WriteLine(item); }
運行結果:
Where擴展方法只是生成了一個迭代器,告訴這個迭代器原始的集合,以及如何篩選的方法,到調用的時候,才真正的過濾數據,這就是Linq延遲載入的原理。