迭代器模式是設計模式中行為模式(behavioral pattern)的一個例子,他是一種簡化對象間通訊的模式,也是一種非常容易理解和使用的模式。簡單來說,迭代器模式使得你能夠獲取到序列中的所有元素 而不用關心是其類型是array,list,linked list或者是其他什麼序列結構。這一點使得能夠非常高效的構建數據處理通道(data pipeline)--即數據能夠進入處理通道,進行一系列的變換,或者過濾,然後得到結果。事實上,這正是LINQ的核心模式。
在.NET中,迭代器模式被IEnumerator和IEnumerable及其對應的泛型接口所封裝。如果一個類實現了IEnumerable接 口,那麼就能夠被迭代;調用GetEnumerator方法將返回IEnumerator接口的實現,它就是迭代器本身。迭代器類似數據庫中的游標,他是 數據序列中的一個位置記錄。迭代器只能向前移動,同一數據序列中可以有多個迭代器同時對數據進行操作。
在C#1中已經內建了對迭代器的支持,那就是foreach語句。使得能夠進行比for循環語句更直接和簡單的對集合的迭代,編譯器會將 foreach編譯來調用GetEnumerator和MoveNext方法以及Current屬性,如果對象實現了IDisposable接口,在迭代 完成之後會釋放迭代器。但是在C#1中,實現一個迭代器是相對來說有點繁瑣的操作。C#2使得這一工作變得大為簡單,節省了實現迭代器的不少工作。
接下來,我們來看如何實現一個迭代器以及C#2對於迭代器實現的簡化,然後再列舉幾個迭代器在現實生活中的例子。
假設我們需要實現一個基於環形緩沖的新的集合類型。我們將實現IEnumerable接口,使得用戶能夠很容易的利用該集合中的所有元素。我們的忽 略其他細節,將注意力僅僅集中在如何實現迭代器上。集合將值存儲在數組中,集合能夠設置迭代的起始點,例如,假設集合有5個元素,你能夠將起始點設為2, 那麼迭代輸出為2,3,4,0,最後是1. 為了能夠簡單展示,我們提供了一個設置值和起始點的構造函數。使得我們能夠以下面這種方式遍歷集合:
object[] values = { "a", "b", "c", "d", "e" }; IterationSample collection = new IterationSample(values, 3); foreach (object x in collection) { Console.WriteLine(x); }
由於我們將起始點設置為3,所以集合輸出的結果是d,e,a,b及c,現在,我們來看如何實現 IterationSample 類的迭代器:
class IterationSample : IEnumerable { Object[] values; Int32 startingPoint; public IterationSample(Object[] values, Int32 startingPoint) { this.values = values; this.startingPoint = startingPoint; } public IEnumerator GetEnumerator() { throw new NotImplementedException(); } }
我們還沒有實現GetEnumerator方法,但是如何寫GetEnumerator部分的邏輯呢,第一就是要將游標的當前狀態存在某一個地方。一方面 是迭代器模式並不是一次返回所有的數據,而是客戶端一次只請求一個數據。這就意味著我們要記錄客戶當前請求到了集合中的那一個記錄。C#2編譯器對於迭代 器的狀態保存為我們做了很多工作。 現在來看看,要保存哪些狀態以及狀態存在哪個地方,設想我們試圖將狀態保存在IterationSample集合中,使得它實現IEnumerator和 IEnumerable方法。咋一看,看起來可能,畢竟數據在正確的地方,包括起始位置。我們的GetEnumerator方法僅僅返回this。但是這 種方法有一個很重要的問題,如果GetEnumerator方法調用多次,那麼多個獨立的迭代器就會返回。例如,我們可以使用兩個嵌套的foreach語 句,來獲取所有可能的值對。這兩個迭代需要彼此獨立。這意味著我們需要每次調用GetEnumerator時返回的兩個迭代器對象必須保持獨立。我們仍舊 可以直接在IterationSample類中通過相應函數實現。但是我們的類擁有了多個職責,這位背了單一職責原則。因此,我們來創建另外一個類來實現 迭代器本身。我們使用C#中的內部類來實現這一邏輯。代碼如下:
class IterationSampleEnumerator : IEnumerator { IterationSample parent;//迭代的對象 #1 Int32 position;//當前游標的位置 #2 internal IterationSampleEnumerator(IterationSample parent) { this.parent = parent; position = -1;// 數組元素下標從0開始,初始時默認當前游標設置為 -1,即在第一個元素之前, #3 } public bool MoveNext() { if (position != parent.values.Length) //判斷當前位置是否為最後一個,如果不是游標自增 #4 { position++; } return position < parent.values.Length; } public object Current { get { if (position == -1 || position == parent.values.Length)//第一個之前和最後一個自後的訪問非法 #5 { throw new InvalidOperationException(); } Int32 index = position + parent.startingPoint;//考慮自定義開始位置的情況 #6 index = index % parent.values.Length; return parent.values[index]; } } public void Reset() { position = -1;//將游標重置為-1 #7 } }
要實現一個簡單的迭代器需要手動寫這麼多的代碼:需要記錄迭代的原始集合#1,記錄當前游標位置#2,返回元素時,根據 當前游標和數組定義的起始位置設置定迭代器在數組中的位置#6。初始化時,將當前位置設定在第一個元素之前#3,當第一次調用迭代器時首先需要調用 MoveNext,然後再調用Current屬性。在游標自增時對當前位置進行條件判斷#4,使得即使當第一次調用MoveNext時沒有可返回的元素也 不至於出錯#5。重置迭代器時,我們將當前游標的位置還原到第一個元素之前#7。 除了結合當前游標位置和自定義的起始位置返回正確的值這點容易出錯外,上面的代碼非常直觀。現在,只需要在IterationSample
類的GetEnumerator
方法中返回我們當才編寫的迭代類即可:
public IEnumerator GetEnumerator() { return new IterationSampleEnumerator(this); }
值得注意的是,上面只是一個相對簡單的例子,沒有太多的狀態需要跟蹤,不用檢查集合在迭代的過程中是否發生了變化。為了 實現一個簡單的迭代器,在C#1中我們實現了如此多的代碼。在使用Framework自帶的實現了IEnumerable接口的集合時我們使用 foreach很方便,但是當我們書寫自己的集合來實現迭代時需要編寫這麼多的代碼。在C#1中,大概需要40行代碼來實現一個簡單的迭代器,現在看看 C#2對這一過程的改進。
C#2使得迭代變得更加簡單--減少了很多代碼量也使得代碼更加的優雅。下面的代碼展示了再C#2中實現GetEnumerator方法的完整代碼:
public IEnumerator GetEnumerator() { for (int index = 0; index < this.values.Length; index++) { yield return values[(index + startingPoint) % values.Length]; } }
簡單幾行代碼就能夠完全實現IterationSampleIterator
類所需要的功能。方法看起來很普通,除了使用了yield return
。這條語句告訴編譯器這不是一個普通的方法,而是一個需要執行的迭代塊(yield block
),他返回一個IEnumerator
對象,你能夠使用迭代塊來執行迭代方法並返回一個IEnumerable
需要實現的類型,IEnumerator
或者對應的泛型。如果實現的是非泛型版本的接口,迭代塊返的yield type
是Object
類型,否則返回的是相應的泛型類型。例如,如果方法實現IEnumerable<string>
接口,那麼yield
返回的類型就是String類型。 在迭代塊中除了yield return
外,不允許出現普通的return
語句。塊中的所有yield return
語句必須返回和塊的最後返回類型兼容的類型。舉個例子,如果方法定義需要返回IEnumeratble<string>
類型的話,不能yield return
1 。 需要強調的一點是,對於迭代塊,雖然我們寫的方法看起來像是在順序執行,實際上我們是讓編譯器來為我們創建了一個狀態機。這就是在C#1中我們書寫的那部 分代碼---調用者每次調用只需要返回一個值,因此我們需要記住最後一次返回值時,在集合中位置。 當編譯器遇到迭代塊是,它創建了一個實現了狀態機的內部類。這個類記住了我們迭代器的准確當前位置以及本地變量,包括參數。這個類有點類似與我們之前手寫 的那段代碼,他將所有需要記錄的狀態保存為實例變量。下面來看看,為了實現一個迭代器,這個狀態機需要按順序執行的操作:
GetEnumerator
方法中的代碼來准備下一個待返回的數據;Current
屬性是,需要返回yielded
的值;MoveNext
會返回false。
如下的代碼,展示了迭代器的執行流程,代碼輸出(0,1,2,-1)然後終止。
class Program { static readonly String Padding = new String(' ', 30); static IEnumerable<int32> CreateEnumerable() { Console.WriteLine("{0} CreateEnumerable()方法開始", Padding); for (int i = 0; i < 3; i++) { Console.WriteLine("{0}開始 yield {1}", i); yield return i; Console.WriteLine("{0}yield 結束", Padding); } Console.WriteLine("{0} Yielding最後一個值", Padding); yield return -1; Console.WriteLine("{0} CreateEnumerable()方法結束", Padding); } static void Main(string[] args) { IEnumerable<int32> iterable = CreateEnumerable(); IEnumerator<int32> iterator = iterable.GetEnumerator(); Console.WriteLine("開始迭代"); while (true) { Console.WriteLine("調用MoveNext方法……"); Boolean result = iterator.MoveNext(); Console.WriteLine("MoveNext方法返回的{0}", result); if (!result) { break; } Console.WriteLine("獲取當前值……"); Console.WriteLine("獲取到的當前值為{0}", iterator.Current); } Console.ReadKey(); } }
從輸出結果中可以看出一下幾點:
MoveNext
,CreateEnumerable
中的方法才被調用。MoveNext
的時候,已經做好了所有操作,返回Current
屬性並沒有執行任何代碼。yield return
之後就停止執行,等待下一次調用MoveNext
方法的時候繼續執行。yield return
語句。yield return
執行完成後,代碼並沒有終止。調用MoveNext
返回false使得方法結束。第一點尤為重要:這意味著,不能在迭代塊中寫任何在方法調用時需要立即執行的代碼--比如說參數驗證。如果將參數驗證放在迭代塊中,那麼他將不能夠很好的起作用,這是經常會導致的錯誤的地方,而且這種錯誤不容易發現。 下面來看如何停止迭代,以及finally
語句塊的特殊執行方式。
在普通的方法中,return
語句通常有兩種作用,一是返回調用者執行的結果。二是終止方法的執行,在終止之前執行finally
語句中的方法。在上面的例子中,我們看到了yield return
語句只是短暫的退出了方法,在MoveNext
再次調用的時候繼續執行。在這裡我們沒有寫finally
語句塊。如何真正的退出方法,退出方法時finnally
語句塊如何執行,下面來看看一個比較簡單的結構:yield break
語句塊。 使用 yield break 結束一個迭代
static IEnumerable<int32> CountWithTimeLimit(DateTime limit) { try { for (int i = 1; i <= 100; i++) { if (DateTime.Now >= limit) { yield break; } yield return i; } } finally { Console.WriteLine("停止迭代!"); Console.ReadKey(); } } static void Main(string[] args) { DateTime stop = DateTime.Now.AddSeconds(2); foreach (Int32 i in CountWithTimeLimit(stop)) { Console.WriteLine("返回 {0}", i); Thread.Sleep(300); } }
轉載自:http://www.yamatamain.com/article/21/1.html