一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

【轉(zhuǎn)】【C#】迭代器IEnumerable和IEnumerator

 根的情義 2017-03-16

  迭代器模式是設(shè)計(jì)模式中行為模式(behavioral pattern)的一個(gè)例子,他是一種簡化對(duì)象間通訊的模式,也是一種非常容易理解和使用的模式。簡單來說,迭代器模式使得你能夠獲取到序列中的所有元素而不用關(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及其對(duì)應(yīng)的泛型接口所封裝。如果一個(gè)類實(shí)現(xiàn)了IEnumerable接口,那么就能夠被迭代;調(diào)用GetEnumerator方法將返回IEnumerator接口的實(shí)現(xiàn),它就是迭代器本身。迭代器類似數(shù)據(jù)庫中的游標(biāo),他是數(shù)據(jù)序列中的一個(gè)位置記錄。迭代器只能向前移動(dòng),同一數(shù)據(jù)序列中可以有多個(gè)迭代器同時(shí)對(duì)數(shù)據(jù)進(jìn)行操作。

    在C#1中已經(jīng)內(nèi)建了對(duì)迭代器的支持,那就是foreach語句。使得能夠進(jìn)行比for循環(huán)語句更直接和簡單的對(duì)集合的迭代,編譯器會(huì)將foreach編譯來調(diào)用GetEnumerator和MoveNext方法以及Current屬性,如果對(duì)象實(shí)現(xiàn)了IDisposable接口,在迭代完成之后會(huì)釋放迭代器。但是在C#1中,實(shí)現(xiàn)一個(gè)迭代器是相對(duì)來說有點(diǎn)繁瑣的操作。C#2使得這一工作變得大為簡單,節(jié)省了實(shí)現(xiàn)迭代器的不少工作。

接下來,我們來看如何實(shí)現(xiàn)一個(gè)迭代器以及C#2對(duì)于迭代器實(shí)現(xiàn)的簡化,然后再列舉幾個(gè)迭代器在現(xiàn)實(shí)生活中的例子。

1. C#1:手動(dòng)實(shí)現(xiàn)迭代器的繁瑣

    假設(shè)我們需要實(shí)現(xiàn)一個(gè)基于環(huán)形緩沖的新的集合類型。我們將實(shí)現(xiàn)IEnumerable接口,使得用戶能夠很容易的利用該集合中的所有元素。我們的忽略其他細(xì)節(jié),將注意力僅僅集中在如何實(shí)現(xiàn)迭代器上。集合將值存儲(chǔ)在數(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ù)。使得我們能夠以下面這種方式遍歷集合:

復(fù)制代碼
object[] values = { 'a', 'b', 'c', 'd', 'e' }; IterationSample collection = new IterationSample(values, 3); foreach (object x in collection) { Console.WriteLine(x); }
復(fù)制代碼

由于我們將起始點(diǎn)設(shè)置為3,所以集合輸出的結(jié)果是d,e,a,b及c,現(xiàn)在,我們來看如何實(shí)現(xiàn) IterationSample 類的迭代器:

復(fù)制代碼
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(); } }
復(fù)制代碼

       

復(fù)制代碼
class IterationSampleEnumerator : IEnumerator { IterationSample parent;//迭代的對(duì)象 #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 } }
復(fù)制代碼

  要實(shí)現(xiàn)一個(gè)簡單的迭代器需要手動(dòng)寫這么多的代碼:需要記錄迭代的原始集合#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í)對(duì)當(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ò)外,上面的代碼非常直觀?,F(xiàn)在,只需要在IterationSample類的GetEnumerator方法中返回我們當(dāng)才編寫的迭代類即可:

我們還沒有實(shí)現(xiàn)GetEnumerator方法,但是如何寫GetEnumerator部分的邏輯呢,第一就是要將游標(biāo)的當(dāng)前狀態(tài)存在某一個(gè)地方。一方面是迭代器模式并不是一次返回所有的數(shù)據(jù),而是客戶端一次只請(qǐng)求一個(gè)數(shù)據(jù)。這就意味著我們要記錄客戶當(dāng)前請(qǐng)求到了集合中的那一個(gè)記錄。C#2編譯器對(duì)于迭代器的狀態(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ú)立的迭代器就會(huì)返回。例如,我們可以使用兩個(gè)嵌套的foreach語句,來獲取所有可能的值對(duì)。這兩個(gè)迭代需要彼此獨(dú)立。這意味著我們需要每次調(diào)用GetEnumerator時(shí)返回的兩個(gè)迭代器對(duì)象必須保持獨(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)這一邏輯。代碼如下:

public IEnumerator GetEnumerator() { return new IterationSampleEnumerator(this); }

值得注意的是,上面只是一個(gè)相對(duì)簡單的例子,沒有太多的狀態(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對(duì)這一過程的改進(jìn)。

2. C#2:通過yield語句簡化迭代

2.1 引入迭代塊(iterator)和yield return 語句

C#2使得迭代變得更加簡單--減少了很多代碼量也使得代碼更加的優(yōu)雅。下面的代碼展示了再C#2中實(shí)現(xiàn)GetEnumerator方法的完整代碼:

復(fù)制代碼
public IEnumerator GetEnumerator() { for (int index = 0; index < this.values.Length; index ) { yield return values[(index startingPoint) % values.Length]; } }
復(fù)制代碼

簡單幾行代碼就能夠完全實(shí)現(xiàn)IterationSampleIterator類所需要的功能。方法看起來很普通,除了使用了yield return。這條語句告訴編譯器這不是一個(gè)普通的方法,而是一個(gè)需要執(zhí)行的迭代塊(yield block),他返回一個(gè)IEnumerator對(duì)象,你能夠使用迭代塊來執(zhí)行迭代方法并返回一個(gè)IEnumerable需要實(shí)現(xiàn)的類型,IEnumerator或者對(duì)應(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)是,對(duì)于迭代塊,雖然我們寫的方法看起來像是在順序執(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í)行的操作:

  • 它需要一些初始的狀態(tài)
  • 當(dāng)MoveNext被調(diào)用時(shí),他需要執(zhí)行GetEnumerator方法中的代碼來準(zhǔn)備下一個(gè)待返回的數(shù)據(jù)。
  • 當(dāng)調(diào)用Current屬性是,需要返回yielded的值。
  • 需要知道什么時(shí)候迭代結(jié)束是,MoveNext會(huì)返回false

下面來看看迭代器的執(zhí)行順序。

 

2.2 迭代器的執(zhí)行流程

如下的代碼,展示了迭代器的執(zhí)行流程,代碼輸出(0,1,2,-1)然后終止。

復(fù)制代碼
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(); } }
復(fù)制代碼

為了展示迭代的細(xì)節(jié),以上代碼使用了while循環(huán),正常情況下一般使用foreach。和上次不同,這次在迭代方法中我們返回的是IEnumerable;對(duì)象而不是IEnumerator;對(duì)象。通常,為了實(shí)現(xiàn)IEnumerable接口,只需要返回IEnumerator對(duì)象即可;如果自是想從一個(gè)方法中返回一些列的數(shù)據(jù),那么使用IEnumerable.以下是輸出結(jié)果:

 

從輸出結(jié)果中可以看出一下幾點(diǎn):

  • 直到第一次調(diào)用MoveNext,CreateEnumerable中的方法才被調(diào)用。
  • 在調(diào)用MoveNext的時(shí)候,已經(jīng)做好了所有操作,返回Current屬性并沒有執(zhí)行任何代碼。
  • 代碼在yield return之后就停止執(zhí)行,等待下一次調(diào)用MoveNext方法的時(shí)候繼續(xù)執(zhí)行。
  • 在方法中可以有多個(gè)yield return語句。
  • 在最后一個(gè)yield return執(zhí)行完成后,代碼并沒有終止。調(diào)用MoveNext返回false使得方法結(jié)束。

    第一點(diǎn)尤為重要:這意味著,不能在迭代塊中寫任何在方法調(diào)用時(shí)需要立即執(zhí)行的代碼--比如說參數(shù)驗(yàn)證。如果將參數(shù)驗(yàn)證放在迭代塊中,那么他將不能夠很好的起作用,這是經(jīng)常會(huì)導(dǎo)致的錯(cuò)誤的地方,而且這種錯(cuò)誤不容易發(fā)現(xiàn)。

    下面來看如何停止迭代,以及finally語句塊的特殊執(zhí)行方式。

 

2.3 迭代器的特殊執(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è)迭代

    通常我們要做的是使方法只有一個(gè)退出點(diǎn),通常,多個(gè)退出點(diǎn)的程序會(huì)使得代碼不易閱讀,特別是使用try catch finally等語句塊進(jìn)行資源清理以及異常處理的時(shí)候。在使用迭代塊的時(shí)候也會(huì)遇到這樣的問題,但如果你想早點(diǎn)退出迭代,那么使用yield break就能達(dá)到想要的效果。他能夠馬上終止迭代,使得下一次調(diào)用MoveNext的時(shí)候返回false。

下面的代碼演示了從1迭代到100,但是時(shí)間超時(shí)的時(shí)候就停止了迭代。

復(fù)制代碼
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); } }
復(fù)制代碼

下圖是輸出結(jié)果,可以看出迭代語句正常終止,yield return語句和普通方法中的return語句一樣,下面來看看finally語句塊是什么時(shí)候以及如何執(zhí)行的。

 

Finally語句塊的執(zhí)行

    通常,finally語句塊在當(dāng)方法執(zhí)行退出特定區(qū)域時(shí)就會(huì)執(zhí)行。迭代塊中的finally語句和普通方法中的finally語句塊不一樣。就像我們看到的,yield return語句停止了方法的執(zhí)行,而不是退出方法,根據(jù)這一邏輯,在這種情況下,finally語句塊中的語句不會(huì)執(zhí)行。

    但當(dāng)碰到y(tǒng)ield break語句的時(shí)候,就會(huì)執(zhí)行finally 語句塊,這根普通方法中的return一樣。一般在迭代塊中使用finally語句來釋放資源,就像使用using語句一樣。

    下面來看finally語句如何執(zhí)行。

   不管是迭代到了100次或者是由于時(shí)間到了停止了迭代,或者是拋出了異常,finally語句總會(huì)執(zhí)行,但是在有些情況下,我們不想讓finally語句塊被執(zhí)行。

    只有在調(diào)用MoveNext后迭代塊中的語句才會(huì)執(zhí)行,那么如果不掉用MoveNext呢,如果調(diào)用幾次MoveNext然后停止調(diào)用,結(jié)果會(huì)怎么樣呢?請(qǐng)看下面的代碼?

復(fù)制代碼
DateTime stop = DateTime.Now.AddSeconds(2); foreach (Int32 i in CountWithTimeLimit(stop)) { if (i > 3) { Console.WriteLine('返回中^'); return; } Thread.Sleep(300); }
復(fù)制代碼

   在forech中,return語句之后,因?yàn)镃ountWithTimeLimit中有finally塊所以代碼繼續(xù)執(zhí)行CountWithTimeLimit中的finally語句塊。foreach語句會(huì)調(diào)用GetEnumerator返回的迭代器的Dispose方法。在結(jié)束迭代之前調(diào)用包含迭代塊的迭代器的Dispose方法時(shí),狀態(tài)機(jī)會(huì)執(zhí)行在迭代器范圍內(nèi)處于暫停狀態(tài)下的代碼范圍內(nèi)的所有finally塊,這有點(diǎn)復(fù)雜,但是結(jié)果很容易解釋:只有使用foreach調(diào)用迭代,迭代塊中的finally塊會(huì)如期望的那樣執(zhí)行。下面可以用代碼驗(yàn)證以上結(jié)論:

復(fù)制代碼
IEnumerable<Int32> iterable = CountWithTimeLimit(stop); IEnumerator<Int32> iterator = iterable.GetEnumerator(); iterator.MoveNext(); Console.WriteLine('返回 {0}', iterator.Current); iterator.MoveNext(); Console.WriteLine('返回 {0}', iterator.Current); Console.ReadKey();
復(fù)制代碼

代碼輸出如下:

上圖可以看出,停止迭代沒有打印出來,當(dāng)我們手動(dòng)調(diào)用iterator的Dispose方法時(shí),會(huì)看到如下的結(jié)果。在迭代器迭代結(jié)束前終止迭代器的情況很少見,也很少不使用foreach語句而是手動(dòng)來實(shí)現(xiàn)迭代,如果要手動(dòng)實(shí)現(xiàn)迭代,別忘了在迭代器外面使用using語句,以確保能夠執(zhí)行迭代器的Dispose方法進(jìn)而執(zhí)行finally語句塊。 

下面來看看微軟對(duì)迭代器的一些實(shí)現(xiàn)中的特殊行為:

 

2.4 迭代器執(zhí)行中的特殊行為

 

    如果使用C#2的編譯器將迭代塊編譯,然后使用ildsam或者Reflector查看生成的IL代碼,你會(huì)發(fā)現(xiàn)在幕后編譯器回味我們生成了一些嵌套的類型(nested type).下圖是使用Ildsam來查看生成的IL ,最下面兩行是代碼中的的兩個(gè)靜態(tài)方法,上面藍(lán)色的<CountWithTimeLimit>d_0是編譯器為我們生成的類(尖括號(hào)只是類名,和泛型無關(guān)),代碼中可以看出該類實(shí)現(xiàn)了那些接口,以及有哪些方法和字段。大概和我們手動(dòng)實(shí)現(xiàn)的迭代器結(jié)構(gòu)類似。

真正的代碼邏輯實(shí)在MoveNext方法中執(zhí)行的,其中有一個(gè)大的switch語句。幸運(yùn)的是,作為一名開發(fā)人員沒必要了解這些細(xì)節(jié),但一些迭代器執(zhí)行的方式還是值得注意的:

  • 在MoveNext方法第一次執(zhí)行之前,Current屬性總是返回迭代器返回類型的默認(rèn)的值。例如IEnumeratble返回的是Int32類型,那么默認(rèn)初始值是0,所以在調(diào)用MoveNext方法之前調(diào)用Current屬性就會(huì)返回0。
  • MoveNext方法返回false后,Current屬性總是返回最后迭代的那個(gè)值。
  • Reset方法一般會(huì)拋出異常,而在本文開始代碼中,我們手動(dòng)實(shí)現(xiàn)一個(gè)迭代器時(shí)在Reset中能夠正確執(zhí)行邏輯。
  • 編譯器為我們產(chǎn)生的嵌套類會(huì)同時(shí)實(shí)現(xiàn)IEnumerator的泛型和非泛型版本(恰當(dāng)?shù)臅r(shí)候還會(huì)實(shí)現(xiàn)IEnumerable的泛型和非泛型版本).

   沒有正確實(shí)現(xiàn)Reset方法是有原因的--編譯器不知道需要使用怎樣的邏輯來從新設(shè)置迭代器。很多人認(rèn)為不應(yīng)該有Reset方法,很多集合并不支持,因此調(diào)用者不應(yīng)該依賴這一方法。

   實(shí)現(xiàn)其它接口沒有壞處。方法中返回IEnumerable接口,他實(shí)現(xiàn)了五個(gè)接口(包括IDisposable),作為一個(gè)開發(fā)者不用擔(dān)心這些。同時(shí)實(shí)現(xiàn)IEnumerable和IEnumerator接口并不常見,編譯器為了使迭代器的行為總是正常,并且為能夠在當(dāng)前的線程中僅僅需要迭代一個(gè)集合就能創(chuàng)建一個(gè)單獨(dú)的嵌套類型才這么做的。

   Current屬性的行為有些古怪,他保存了迭代器的最后一個(gè)返回值并且阻止了垃圾回收期進(jìn)行收集。

因此,自動(dòng)實(shí)現(xiàn)的迭代器方法有一些小的缺陷,但是明智的開發(fā)者不會(huì)遇到任何問題,使用他能夠節(jié)省很多代碼量,使得迭代器的使用程度比C#1中要廣。下面來看在實(shí)際開發(fā)中迭代器簡化代碼的地方。

3.實(shí)際開發(fā)中使用迭代的例子

3.1 從時(shí)間段中迭代日期

在涉及到時(shí)間區(qū)段時(shí),通常會(huì)使用循環(huán),代碼如下:

for (DateTime day = timetable.StartDate; day < timetable.EndDate; day=day.AddDays(1)) { …… }

循環(huán)有時(shí)沒有迭代直觀和有表現(xiàn)力,在本例中,可以理解為“時(shí)間區(qū)間中的每一天”,這正是foreach使用的場景。因此上述循環(huán)如果寫成迭代,代碼會(huì)更美觀:

foreach(DateTime day in timetable.DateRange) { …… }

在C#1.0中要實(shí)現(xiàn)這個(gè)需要下一定功夫。到了C#2.0就變得簡單了。在timetable類中,只需要添加一個(gè)屬性:

復(fù)制代碼
public IEnumerable<DateTime> DateRange { get { for (DateTime day=StartDate ; day < =EndDate; day=day.AddDays(1)) { yield return day; } } }
復(fù)制代碼

只是將循環(huán)移動(dòng)到了timetable類的內(nèi)部,但是經(jīng)過這一改動(dòng),使得封裝變得更為良好。DateRange屬性只是遍歷時(shí)間區(qū)間中的每一天,每一次返回一天。如果想要使得邏輯變得復(fù)雜一點(diǎn),只需要改動(dòng)一處。這一小小的改動(dòng)使得代碼的可讀性大大增強(qiáng),接下來可以考慮將這個(gè)Range擴(kuò)展為泛型Range<T>。

3.2迭代讀取文件中的每一行

讀取文件時(shí),我們經(jīng)常會(huì)書寫這樣的代碼:

復(fù)制代碼
using (TextReader reader=File.OpenText(fileName)) { String line; while((line=reader.ReadLine())!=null) { …… } }
復(fù)制代碼

這一過程中有4個(gè)環(huán)節(jié):

  • 如何獲取TextReader
  • 管理TextReader的生命周期
  • 通過TextReader.ReadLine迭代所有的行
  • 對(duì)行進(jìn)行處理

可以從兩個(gè)方面對(duì)這一過程進(jìn)行改進(jìn):可以使用委托--可以寫一個(gè)擁有reader和一個(gè)代理作為參數(shù)的輔助方法,使用代理方法來處理每一行,最后關(guān)閉reader,這經(jīng)常被用來展示閉包和代理。還有一種更為優(yōu)雅更符合LINQ方式的改進(jìn)。除了將邏輯作為方法參數(shù)傳進(jìn)去,我們可以使用迭代來迭代一次迭代一行代碼,這樣我們就可以使用foreach語句。代碼如下:

復(fù)制代碼
static IEnumerable<String> ReadLines(String fileName) { using (TextReader reader = File.OpenText(fileName)) { String line; while ((line = reader.ReadLine()) != null) { yield return line; } } }
復(fù)制代碼

這樣就可以使用如下foreach方法來讀取文件了:

foreach (String line in ReadLines('test.txt')) { Console.WriteLine(line); }

  方法的主體部分和之前的一樣,使用yield return返回了讀取到的每一行,只是在迭代結(jié)束后有點(diǎn)不同。之前的操作,先打開文檔,每一次讀取一行,然后在讀取結(jié)束時(shí)關(guān)閉reader。雖然”當(dāng)讀取結(jié)束時(shí)”和之前方法中使用using相似,但當(dāng)使用迭代時(shí)這個(gè)過程更加明顯。

這就是為什么foreach迭代結(jié)束后會(huì)調(diào)用迭代器的dispose方法這么重要的原因了,這個(gè)操作能夠保證reader能夠得到釋放。迭代方法中的using語句塊類似與try/finally語句塊;finally語句在讀取文件結(jié)束或者當(dāng)我們顯示調(diào)用IEnumerator<String> 的Dispose方法時(shí)都會(huì)執(zhí)行。可能有時(shí)候會(huì)通過ReadLine().GetEnumerator()的方式返回IEnumerator<String> ,進(jìn)行手動(dòng)迭代而沒有調(diào)用Dispose方法,就會(huì)產(chǎn)生資源泄漏。通常會(huì)使用foreach語句來迭代循環(huán),所以這個(gè)問題很少會(huì)出現(xiàn)。但是還是有必要意識(shí)到這個(gè)潛在的問題。

      該方法封裝了前三個(gè)步驟,這可能有點(diǎn)苛刻。將生命周期和方法進(jìn)行封裝是有必要的,現(xiàn)在擴(kuò)展一下,假如我們要從網(wǎng)絡(luò)上讀取一個(gè)流文件,或者我們想使用UTF-8編碼的方法,我們需要將第一個(gè)部分暴漏給方法調(diào)用者,使得方法的調(diào)用簽名大致如下:

static IEnumerable<String> ReadLines(TextReader reader)

這樣有很多不好的地方,我們想對(duì)reader有絕對(duì)的控制,使得調(diào)用者能夠在結(jié)束后能進(jìn)行資源清理。問題在于,如果在第一次調(diào)用MoveNext()之前出現(xiàn)錯(cuò)誤,那么我們就沒有機(jī)會(huì)進(jìn)行資源清理工作了。IEnumerable<String>自身不能釋放,他存儲(chǔ)了某個(gè)狀態(tài)需要被清理。另一個(gè)問題是如果GetEnumerator被調(diào)用兩次,我們本意是返回兩個(gè)獨(dú)立的迭代器,然后他們卻使用了相同的reader。一種方法是,將返回類型改為IEnumerator<String>,但這樣的話,不能使用foreach進(jìn)行迭代,而且如果沒有執(zhí)行到MoveNext方法的話,資源也得不到清理。

   幸運(yùn)的是,有一種方法可以解決以上問題。就像代碼不必立即執(zhí)行,我們也不需要reader立即執(zhí)行。我們可以提供一個(gè)接口實(shí)現(xiàn)“如果需要一個(gè)TextReader,我們可以提供”。在.NET 3.5中有一個(gè)代理,簽名如下:

public delegate TResult Func<TResult>()

代理沒有參數(shù),返回和類型參數(shù)相同的類型。我們想獲得TextReader對(duì)象,所以可以使用Func<TextReader>,代碼如下:

復(fù)制代碼
using (TextReader reader=provider()) { String line; while ((line=reader.ReadLine())!=null) { yield return line; } }
復(fù)制代碼

3.3 使用迭代塊和迭代條件來對(duì)集合進(jìn)行進(jìn)行惰性過濾

   LINQ允許對(duì)內(nèi)存集合或者數(shù)據(jù)庫等多種數(shù)據(jù)源用簡單強(qiáng)大的方式進(jìn)行查詢。雖然C#2沒有對(duì)查詢表達(dá)式,lambda表達(dá)及擴(kuò)展方法進(jìn)行集成。但是我們也能達(dá)到類似的效果。

   LINQ的一個(gè)核心的特征是能夠使用where方法對(duì)數(shù)據(jù)進(jìn)行過濾。提供一個(gè)集合以及過濾條件代理,過濾的結(jié)果就會(huì)在迭代的時(shí)候通過惰性匹配,每匹配一個(gè)過濾條件就返回一個(gè)結(jié)果。這有點(diǎn)像List<T>.FindAll方法,但是LINQ支持對(duì)所有實(shí)現(xiàn)了IEnumerable<T>接口的對(duì)象進(jìn)行惰性求值。雖然從C#3開始支持LINQ,但是我們也可以使用已有的知識(shí)在一定程度上實(shí)現(xiàn)LINQ的Where語句。代碼如下:

復(fù)制代碼
public static IEnumerable<T> Where<T>(IEnumerable<T> source, Predicate<T> predicate) { if (source == null || predicate == null) throw new ArgumentNullException(); return WhereImpl(source, predicate); } private static IEnumerable<T> WhereImpl<T>(IEnumerable<T> source, Predicate<T> predicate) { foreach (T item in source) { if (predicate(item)) yield return item; } } IEnumerable<String> lines = ReadLines('FakeLinq.cs'); Predicate<String> predicate = delegate(String line) { return line.StartsWith('using'); };
復(fù)制代碼

 如上代碼中,我們將整個(gè)實(shí)現(xiàn)分為了兩個(gè)部分,參數(shù)驗(yàn)證和具體邏輯。雖然看起來奇怪,但是對(duì)于錯(cuò)誤處理來說是很有必要的。如果將這兩個(gè)部分方法放到一個(gè)方法中,如果用戶調(diào)用了Where<String>(null,null),將不會(huì)發(fā)生任何問題,至少我們期待的異常沒有拋出。這是由于迭代塊的惰性求值機(jī)制產(chǎn)生的。在用戶迭代的時(shí)候第一次調(diào)用MoveNext方法之前,方法主體中的代碼不會(huì)執(zhí)行,就像在2.2節(jié)中看到的那樣。如果你想急切的對(duì)方法的參數(shù)進(jìn)行判斷,那么沒有一個(gè)地方能夠延緩異常,這使得bug的追蹤變得困難。標(biāo)準(zhǔn)的做法如上代碼,將方法分為兩部分,一部分像普通方法那樣對(duì)參數(shù)進(jìn)行驗(yàn)證,另一部分代碼使用迭代塊對(duì)主體邏輯數(shù)據(jù)進(jìn)行惰性處理。

    迭代塊的主體很直觀,對(duì)集合中的逐個(gè)元素,使用predict代理方法進(jìn)行判斷,如果滿足條件,則返回。如果不滿足條件,則迭代下一個(gè),直到滿足條件為止。如果要在C#1中實(shí)現(xiàn)這點(diǎn)邏輯就很困難,特別是實(shí)現(xiàn)其泛型版本。

   后面的那段代碼演示了使用之前的readline方法讀取數(shù)據(jù)然后用我們的where方法來過濾獲取line中以u(píng)sing開頭的行,和用File.ReadAllLines及Array.FindAll<String>實(shí)現(xiàn)這一邏輯的最大的差別是,我們的方法是完全惰性和流線型的(Streaming)。每一次只在內(nèi)存中請(qǐng)求一行并對(duì)其進(jìn)行處理,當(dāng)然如果文件比較小的時(shí)候沒有什么差別,但是如果文件很大,例如上G的日志文件,這種方法的優(yōu)勢就會(huì)顯現(xiàn)出來了。

 

4 總結(jié)

   C#對(duì)許多設(shè)計(jì)模式進(jìn)行了間接的實(shí)現(xiàn),使得實(shí)現(xiàn)這些模式變得很容易。相對(duì)來針對(duì)某一特定的設(shè)計(jì)模式直接實(shí)現(xiàn)的的特性比較少。從foreach代碼中看出,C#1對(duì)迭代器模式進(jìn)行了直接的支持,但是沒有對(duì)進(jìn)行迭代的集合進(jìn)行有效的支持。對(duì)集合實(shí)現(xiàn)一個(gè)正確的IEnumerable很耗時(shí),容易出錯(cuò)也很很枯燥。在C#2中,編譯器為我們做了很多工作,為我們實(shí)現(xiàn)了一個(gè)狀態(tài)機(jī)來實(shí)現(xiàn)迭代。

    本文還展示了和LINQ相似的一個(gè)功能:對(duì)集合進(jìn)行過濾。IEnumerable<T>在LINQ中最重要的一個(gè)接口,如果想要在LINQ To Object上實(shí)現(xiàn)自己的LINQ操作,那么你會(huì)由衷的感嘆這個(gè)接口的強(qiáng)大功能以及C#語言提供的迭代塊的用處。

    本文還展示了實(shí)際項(xiàng)目中使用迭代塊使得代碼更加易讀和邏輯性更好的例子,希望這些例子使你對(duì)理解迭代有所幫助。

 

原文地址:http://www.cnblogs.com/yangecnu/archive/2012/03/17/2402432.html

 

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多

    国产又粗又硬又长又爽的剧情| 午夜免费精品视频在线看| 午夜福利黄片免费观看| 草草草草在线观看视频| 国产亚洲精品香蕉视频播放| 自拍偷拍一区二区三区| 91欧美日韩中在线视频| 欧美午夜一级艳片免费看| 伊人久久青草地婷婷综合| 午夜精品麻豆视频91| 国产激情一区二区三区不卡| 国语对白刺激高潮在线视频| 男女午夜在线免费观看视频| 东京热电东京热一区二区三区| 国产综合香蕉五月婷在线| 午夜福利在线观看免费| 国产午夜免费在线视频| 国产亚洲系列91精品| 日韩精品少妇人妻一区二区| 精品国产亚洲av久一区二区三区| 正在播放国产又粗又长| 国产精品视频一区二区秋霞| 国产日韩欧美在线播放| 国产性情片一区二区三区| 欧美精品亚洲精品一区| 年轻女房东2中文字幕| 人妻熟女欲求不满一区二区| 99亚洲综合精品成人网色播| 日韩蜜桃一区二区三区| 91久久国产福利自产拍| 日本人妻中出在线观看| 日本欧美视频在线观看免费| 国产黑人一区二区三区| 五月天婷亚洲天婷综合网| 亚洲最新的黄色录像在线| 91亚洲国产日韩在线| 在线懂色一区二区三区精品| 色一情一伦一区二区三| 九九热在线视频观看最新| 国产日韩中文视频一区| 日韩偷拍精品一区二区三区|