迭代器模式是設(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ù)。使得我們能夠以下面這種方式遍歷集合: 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();
}
}
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
}
}
要實(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方法的完整代碼: 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對(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í)行的操作:
下面來看看迭代器的執(zhí)行順序。
2.2 迭代器的執(zhí)行流程如下的代碼,展示了迭代器的執(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();
}
}
為了展示迭代的細(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ǎ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í)候就停止了迭代。 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);
}
}
下圖是輸出結(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)看下面的代碼? DateTime stop = DateTime.Now.AddSeconds(2);
foreach (Int32 i in CountWithTimeLimit(stop))
{
if (i > 3)
{
Console.WriteLine('返回中^');
return;
}
Thread.Sleep(300);
}
在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é)論: 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();
代碼輸出如下: 上圖可以看出,停止迭代沒有打印出來,當(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í)行的方式還是值得注意的:
沒有正確實(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è)屬性: public IEnumerable<DateTime> DateRange
{
get
{
for (DateTime day=StartDate ; day < =EndDate; day=day.AddDays(1))
{
yield return day;
}
}
}
只是將循環(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ì)書寫這樣的代碼: using (TextReader reader=File.OpenText(fileName))
{
String line;
while((line=reader.ReadLine())!=null)
{
……
}
}
這一過程中有4個(gè)環(huán)節(jié):
可以從兩個(gè)方面對(duì)這一過程進(jìn)行改進(jìn):可以使用委托--可以寫一個(gè)擁有reader和一個(gè)代理作為參數(shù)的輔助方法,使用代理方法來處理每一行,最后關(guān)閉reader,這經(jīng)常被用來展示閉包和代理。還有一種更為優(yōu)雅更符合LINQ方式的改進(jìn)。除了將邏輯作為方法參數(shù)傳進(jìn)去,我們可以使用迭代來迭代一次迭代一行代碼,這樣我們就可以使用foreach語句。代碼如下: static IEnumerable<String> ReadLines(String fileName)
{
using (TextReader reader = File.OpenText(fileName))
{
String line;
while ((line = reader.ReadLine()) != null)
{
yield return line;
}
}
}
這樣就可以使用如下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>,代碼如下: using (TextReader reader=provider())
{
String line;
while ((line=reader.ReadLine())!=null)
{
yield return line;
}
}
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語句。代碼如下: 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');
};
如上代碼中,我們將整個(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
|
|