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