19.1.1 為什么泛型? 沒(méi)有泛型,一些通用的數(shù)據(jù)結(jié)構(gòu)只能使用object類型來(lái)存貯各種類型的數(shù)據(jù)。例如,下面這個(gè)簡(jiǎn)單的stack類將它的數(shù)據(jù)存放在一個(gè)object數(shù)組中,而它的兩個(gè)方法,push和pop,分別使用object來(lái)接受和返回?cái)?shù)據(jù): public class stack { object[] items; int count; public void push(object item) {...} public object pop() {...} } 盡管使用object類型使得stack類非常靈活,但它也不是沒(méi)有缺點(diǎn)。例如,可以向堆棧中壓入任何類型的值,譬如一個(gè)customer實(shí)例。然而,重新取回一個(gè)值得時(shí)候,必須將pop方法返回的值顯式地轉(zhuǎn)換為合適的類型,書(shū)寫(xiě)這些轉(zhuǎn)換變更要提防運(yùn)行時(shí)類型檢查錯(cuò)誤是很乏味的: stack stack = new stack(); stack.push(new customer()); customer c = (customer)stack.pop(); 如果一個(gè)值類型的值,如int,傳遞給了push方法,它會(huì)自動(dòng)裝箱。而當(dāng)待會(huì)兒取回這個(gè)int值時(shí),必須顯式的類型轉(zhuǎn)換進(jìn)行拆箱: stack stack = new stack(); stack.push(3); int i = (int)stack.pop(); 這種裝箱和拆箱操作增加了執(zhí)行的負(fù)擔(dān),因?yàn)樗鼛?lái)了動(dòng)態(tài)內(nèi)存分配和運(yùn)行時(shí)類型檢查。 stack類的另外一個(gè)問(wèn)題是無(wú)法強(qiáng)制堆棧中的數(shù)據(jù)的種類。確實(shí),一個(gè)customer實(shí)例可以被壓入棧中,而在取回它的時(shí)候會(huì)意外地轉(zhuǎn)換成一個(gè)錯(cuò)誤的類型: stack stack = new stack(); stack.push(new customer()); string s = (string)stack.pop(); 盡管上面的代碼是stack類的一種不正確的用法,但這段代碼從技術(shù)上來(lái)說(shuō)是正確的,并且不會(huì)發(fā)生編譯期間錯(cuò)誤。為題知道這段代碼運(yùn)行的時(shí)候才會(huì)出現(xiàn),這時(shí)會(huì)拋出一個(gè)invalidcastexception異常。 stack類無(wú)疑會(huì)從具有限定其元素類型的能力中獲益。使用泛型,這將成為可能。
19.1.2 建立和使用泛型 泛型提供了一個(gè)技巧來(lái)建立帶有類型參數(shù)(type parameters)的類型。下面的例子聲明了一個(gè)帶有類型參數(shù)t的泛型stack類。類型參數(shù)又類名字后面的定界符“<”和“>”指定。通過(guò)某種類型建立的stack<t>的實(shí)例 可以無(wú)欲轉(zhuǎn)換地接受該種類型的數(shù)據(jù),這強(qiáng)過(guò)于與object相互裝換。類型參數(shù)t扮演一個(gè)占位符的角色,直到使用時(shí)指定了一個(gè)實(shí)際的類型。注意t相當(dāng)于內(nèi)部數(shù)組的數(shù)據(jù)類型、push方法接受的參數(shù)類型和pop方法的返回值類型: public class stack<t> { t[] items; int count; public void push(t item) {...} public t pop() {...} } 使用泛型類stack<t>時(shí),需要指定實(shí)際的類型來(lái)替代t。下面的例子中,指定int作為參數(shù)類型t: stack<int> stack = new stack<int>(); stack.push(3); int x = stack.pop(); stack<int>類型稱為已構(gòu)造類型(constructed type)。在stack<int>類型中出現(xiàn)的所有t被替換為類型參數(shù)int。當(dāng)一個(gè)stack<int>的實(shí)例被創(chuàng)建時(shí),items數(shù)組的本地存貯是int[]而不是object[],這提供了一個(gè)實(shí)質(zhì)的存貯,效率要高過(guò)非泛型的stack。同樣,stack<int>中的push和pop方法只操作int值,如果向堆棧中壓入其他類型的值將會(huì)得到編譯期間的錯(cuò)誤,而且取回一個(gè)值時(shí)不必將它顯示轉(zhuǎn)換為原類型。 泛型可以提供強(qiáng)類型,這意味著例如向一個(gè)customer對(duì)象的堆棧上壓入一個(gè)int將會(huì)產(chǎn)生錯(cuò)誤。這是因?yàn)閟tack<int>只能操作int值,而stack<customer>也只能操作customer對(duì)象。下面例子中的最后兩行會(huì)導(dǎo)致編譯器報(bào)錯(cuò): stack<customer> stack = new stack<customer>(); stack.push(new customer()); customer c = stack.pop(); stack.push(3); // 類型不匹配錯(cuò)誤 int x = stack.pop(); // 類型不匹配錯(cuò)誤 泛型類型的聲明允許任意數(shù)目的類型參數(shù)。上面的stack<t>例子只有一個(gè)類型參數(shù),但一個(gè)泛型的dictionary類可能有兩個(gè)類型參數(shù),一個(gè)是鍵的類型另一個(gè)是值的類型: public class dictionary<k,v> { public void add(k key, v value) {...} public v this[k key] {...} } 使用dictionary<k,v>時(shí),需要提供兩個(gè)類型參數(shù): dictionary<string,customer> dict = new dictionary<string,customer>(); dict.add("peter", new customer()); customer c = dict["peter"];
19.1.3 泛型類型實(shí)例化 和非泛型類型類似,編譯過(guò)的泛型類型也由中間語(yǔ)言(il, intermediate language)指令和元數(shù)據(jù)表示。泛型類型的il表示當(dāng)然已由類型參數(shù)進(jìn)行了編碼。 當(dāng)程序第一次建立一個(gè)已構(gòu)造的泛型類型的實(shí)例時(shí),如stack<int>,.net公共語(yǔ)言運(yùn)行時(shí)中的即時(shí)編譯器(jit, just-in-time)將泛型il和元數(shù)據(jù)轉(zhuǎn)換為本地代碼,并在進(jìn)程中用實(shí)際類型代替類型參數(shù)。后面的對(duì)這個(gè)以構(gòu)造的泛型類型的引用使用相同的本地代碼。從泛型類型建立一個(gè)特定的構(gòu)造類型的過(guò)程稱為泛型類型實(shí)例化(generic type instantiation)。 .net公共語(yǔ)言運(yùn)行時(shí)為每個(gè)由之類型實(shí)例化的泛型類型建立一個(gè)專門(mén)的拷貝,而所有的引用類型共享一個(gè)單獨(dú)的拷貝(因?yàn)椋诒镜卮a級(jí)別上,引用知識(shí)具有相同表現(xiàn)的指針)。
19.1.4 約束 通常,一個(gè)泛型類不會(huì)只是存貯基于某一類型參數(shù)的數(shù)據(jù),他還會(huì)調(diào)用給定類型的對(duì)象的方法。例如,dictionary<k,v>中的add方法可能需要使用compareto方法來(lái)比較鍵值: public class dictionary<k,v> { public void add(k key, v value) { ... if (key.compareto(x) < 0) {...} // 錯(cuò)誤,沒(méi)有compareto方法 ... } } 由于指定的類型參數(shù)k可以是任何類型,可以假定存在的參數(shù)key具有的成員只有來(lái)自object的成員,如equals、gethashcode和tostring;因此上面的例子會(huì)發(fā)生編譯錯(cuò)誤。當(dāng)然可以將參數(shù)key轉(zhuǎn)換成為一具有compareto方法的類型。例如,參數(shù)key可以轉(zhuǎn)換為icomparable: public class dictionary<k,v> { public void add(k key, v value) { ... if (((icomparable)key).compareto(x) < 0) {...} ... } } 當(dāng)這種方案工作時(shí),會(huì)在運(yùn)行時(shí)引起動(dòng)態(tài)類型轉(zhuǎn)換,會(huì)增加開(kāi)銷。更要命的是,它還可能將錯(cuò)誤報(bào)告推遲到運(yùn)行時(shí)。如果一個(gè)鍵沒(méi)有實(shí)現(xiàn)icomparable接口,會(huì)拋出invalidcastexception異常。 為了提供更強(qiáng)大的編譯期間類型檢查和減少類型轉(zhuǎn)換,c#允許一個(gè)可選的為每個(gè)類型參數(shù)提供的約束(constraints)列表。一個(gè)類型參數(shù)的約束指定了一個(gè)類型必須遵守的要求,使得這個(gè)類型參數(shù)能夠作為一個(gè)變量來(lái)使用。約束由關(guān)鍵字where來(lái)聲明,后跟類型參數(shù)的名字,再后是一個(gè)類或接口類型的列表,或構(gòu)造器約束new()。 要想使dictionary<k,v>類能保證鍵值始終實(shí)現(xiàn)了icomparable接口,類的聲明中應(yīng)該對(duì)類型參數(shù)k指定一個(gè)約束: public class dictionary<k,v> where k: icomparable { public void add(k key, v value) { ... if (key.compareto(x) < 0) {...} ... } } 通過(guò)這個(gè)聲明,編譯器能夠保證所有提供給類型參數(shù)k的類型都實(shí)現(xiàn)了icomparable接口。進(jìn)而,在調(diào)用compareto方法前不再需要將鍵值顯式轉(zhuǎn)換為一個(gè)icomparable接口;一個(gè)受約束的類型參數(shù)類型的值的所有成員都可以直接使用。 對(duì)于給定的類型參數(shù),可以指定任意數(shù)目的接口作為約束,但只能指定一個(gè)類(作為約束)。每一個(gè)被約束的類型參數(shù)都有一個(gè)獨(dú)立的where子句。在下面的例子中,類型參數(shù)k有兩個(gè)接口約束,而類型參數(shù)e有一個(gè)類約束和一個(gè)構(gòu)造器約束: public class entitytable<k,e> where k: icomparable<k>, ipersistable where e: entity, new() { public void add(k key, e entity) { ... if (key.compareto(x) < 0) {...} ... } } 上面例子中的構(gòu)造器約束,new(),保證了作為的e類型變量的類型具有一個(gè)公共、無(wú)參的構(gòu)造器,并允許泛型類使用new e()來(lái)建立該類型的一個(gè)實(shí)例。 類型參數(shù)約束的使用要小心。盡管它們提供了更強(qiáng)大的編譯期間類型檢查并在一些情況下改進(jìn)了性能,它還是限制了泛型類型的使用。例如,一個(gè)泛型類list<t>可能約束t實(shí)現(xiàn)icomparable接口以便sort方法能夠比較其中的元素。然而,這么做使list<t>不能用于那些沒(méi)有實(shí)現(xiàn)icomparable接口的類型,盡管在這種情況下sort方法從來(lái)沒(méi)被實(shí)際調(diào)用過(guò)。
19.1.5 泛型方法 有的時(shí)候一個(gè)類型參數(shù)并不是整個(gè)類所必需的,而只用于一個(gè)特定的方法中。通常,這種情況發(fā)生在建立一個(gè)需要一個(gè)泛型類型作為參數(shù)的方法時(shí)。例如,在使用前面描述過(guò)的stack<t>類時(shí),一種公共的模式就是在一行中壓入多個(gè)值,如果寫(xiě)一個(gè)方法通過(guò)單獨(dú)調(diào)用它類完成這一工作會(huì)很方便。對(duì)于一個(gè)特定的構(gòu)造過(guò)的類型,如stack<int>,這個(gè)方法看起來(lái)會(huì)是這樣: void pushmultiple(stack<int> stack, params int[] values) { foreach (int value in values) stack.push(value); } 這個(gè)方法可以用于將多個(gè)int值壓入一個(gè)stack<int>: stack<int> stack = new stack<int>(); pushmultiple(stack, 1, 2, 3, 4); 然而,上面的方法只能工作于特定的構(gòu)造過(guò)的類型stack<int>。要想使他工作于任何stack<t>,這個(gè)方法必須寫(xiě)成泛型方法(generic method)。一個(gè)泛型方法有一個(gè)或多個(gè)類型參數(shù),有方法名后面的“<”和“>”限定符指定。這個(gè)類型參數(shù)可以用在參數(shù)列表、返回至和方法體中。一個(gè)泛型的pushmultiple方法看起來(lái)會(huì)是這樣: void pushmultiple<t>(stack<t> stack, params t[] values) { foreach (t value in values) stack.push(value); } 使用這個(gè)方法,可以將多個(gè)元素壓入任何stack<t>中。當(dāng)調(diào)用一個(gè)泛型方法時(shí),要在函數(shù)的調(diào)用中將類型參數(shù)放入尖括號(hào)中。例如: stack<int> stack = new stack<int>(); pushmultiple<int>(stack, 1, 2, 3, 4); 這個(gè)泛型的pushmultiple方法比上面的版本更具可重用性,因?yàn)樗芄ぷ饔谌魏蝧tack<t>,但這看起來(lái)并不舒服,因?yàn)楸仨殲閠提供一個(gè)類型參數(shù)。然而,很多時(shí)候編譯器可以通過(guò)傳遞給方法的其他參數(shù)來(lái)推斷出正確的類型參數(shù),這個(gè)過(guò)程稱為類型推斷(type inferencing)。在上面的例子中,由于第一個(gè)正式的參數(shù)的類型是stack<int>,并且后面的參數(shù)類型都是int,編譯器可以認(rèn)定類型參數(shù)一定是int。因此,在調(diào)用泛型的pushmultiple方法時(shí)可以不用提供類型參數(shù): stack<int> stack = new stack<int>(); pushmultiple(stack, 1, 2, 3, 4);
19.2 匿名方法 實(shí)踐處理方法和其他回調(diào)方法通常需要通過(guò)專門(mén)的委托來(lái)調(diào)用,而不是直接調(diào)用。因此,迄今為止我們還只能將一個(gè)實(shí)踐處理和回調(diào)的代碼放在一個(gè)具體的方法中,再為其顯式地建立委托。相反,匿名方法(anonymous methods)允許將與一個(gè)委托關(guān)聯(lián)的代碼“內(nèi)聯(lián)(in-line)”到使用委托的地方,我們可以很方便地將代碼直接寫(xiě)在委托實(shí)例中。除了看起來(lái)舒服,匿名方法還共享對(duì)本地語(yǔ)句所包含的函數(shù)成員的訪問(wèn)。如果想在命名方法(區(qū)別于匿名方法)中達(dá)成這種共享,需要手動(dòng)創(chuàng)建一個(gè)輔助類并將本地成員“提升(lifting)”到這個(gè)類的域中。 下面的例子展示了從一個(gè)包含一個(gè)列表框、一個(gè)文本框和一個(gè)按鈕的窗體中獲取一個(gè)簡(jiǎn)單的輸入。當(dāng)按鈕按下時(shí)文本框中的文本會(huì)被添加到列表框中。 class inputform: form { listbox listbox; textbox textbox; button addbutton; public myform() { listbox = new listbox(...); textbox = new textbox(...); addbutton = new button(...); addbutton.click += new eventhandler(addclick); } void addclick(object sender, eventargs e) { listbox.items.add(textbox.text); } } 盡管對(duì)按鈕的click事件的響應(yīng)只有一條語(yǔ)句,這條語(yǔ)句也必須放到一個(gè)獨(dú)立的具有完整的參數(shù)列表的方法中,并且要手動(dòng)創(chuàng)建引用該方法的eventhandler委托。使用匿名方法,事件處理的代碼會(huì)變得更加簡(jiǎn)潔: class inputform: form { listbox listbox; textbox textbox; button addbutton; public myform() { listbox = new listbox(...); textbox = new textbox(...); addbutton = new button(...); addbutton.click += delegate { listbox.items.add(textbox.text); }; } } 一個(gè)匿名方法由關(guān)鍵字delegate和一個(gè)可選的參數(shù)列表組成,并將語(yǔ)句放入“{”和“}”限定符中。前面例子中的匿名方法沒(méi)有使用提供給委托的參數(shù),因此可以省略參數(shù)列表。要想訪問(wèn)參數(shù),你名方法應(yīng)該包含一個(gè)參數(shù)列表: addbutton.click += delegate(object sender, eventargs e) { messagebox.show(((button)sender).text); }; 上面的例子中,在匿名方法和eventhandler委托類型(click事件的類型)之間發(fā)生了一個(gè)隱式的轉(zhuǎn)換。這個(gè)隱式的轉(zhuǎn)換是可行的,因?yàn)檫@個(gè)委托的參數(shù)列表和返回值類型和匿名方法是兼容的。精確的兼容規(guī)則如下: • 當(dāng)下面條例中有一條為真時(shí),則委托的參數(shù)列表和匿名方法是兼容的: o 匿名方法沒(méi)有參數(shù)列表且委托沒(méi)有輸出(out)參數(shù)。 o 匿名方法的參數(shù)列表在參數(shù)數(shù)目、類型和修飾符上與委托參數(shù)精確匹配。 • 當(dāng)下面的條例中有一條為真時(shí),委托的返回值與匿名方法兼容: o 委托的返回值類型是void且匿名方法沒(méi)有return語(yǔ)句或其return語(yǔ)句不帶任何表達(dá)式。 o 委托的返回值類型不是void但和匿名方法的return語(yǔ)句關(guān)聯(lián)的表達(dá)式的值可以被顯式地轉(zhuǎn)換為委托的返回值類型。 只有參數(shù)列表和返回值類型都兼容的時(shí)候,才會(huì)發(fā)生匿名類型向委托類型的隱式轉(zhuǎn)換。 下面的例子使用了匿名方法對(duì)函數(shù)進(jìn)行了“內(nèi)聯(lián)(in-lian)”。匿名方法被作為一個(gè)function委托類型傳遞。 using system; delegate double function(double x); class test { static double[] apply(double[] a, function f) { double[] result = new double[a.length]; for (int i = 0; i < a.length; i++) result = f(a); return result; } static double[] multiplyallby(double[] a, double factor) { return apply(a, delegate(double x) { return x * factor; }); } static void main() { double[] a = {0.0, 0.5, 1.0}; double[] squares = apply(a, delegate(double x) { return x * x; }); double[] doubles = multiplyallby(a, 2.0); } } apply方法需要一個(gè)給定的接受double[]元素并返回double[]作為結(jié)果的function。在main方法中,傳遞給apply方法的第二個(gè)參數(shù)是一個(gè)匿名方法,它與function委托類型是兼容的。這個(gè)匿名方法只簡(jiǎn)單地返回每個(gè)元素的平方值,因此調(diào)用apply方法得到的double[]包含了a中每個(gè)值的平方值。 multiplyallby方法通過(guò)將參數(shù)數(shù)組中的每一個(gè)值乘以一個(gè)給定的factor來(lái)建立一個(gè)double[]并返回。為了產(chǎn)生這個(gè)結(jié)果,multiplyallby方法調(diào)用了apply方法,向它傳遞了一個(gè)能夠?qū)?shù)x與factor相乘的匿名方法。 如果一個(gè)本地變量或參數(shù)的作用域包括了匿名方法,則該變量或參數(shù)稱為匿名方法的外部變量(outer variables)。在multiplyallby方法中,a和factor就是傳遞給apply方法的匿名方法的外部變量。通常,一個(gè)局部變量的生存期被限制在塊內(nèi)或與之相關(guān)聯(lián)的語(yǔ)句內(nèi)。然而,一個(gè)被捕獲的外部變量的生存期要擴(kuò)展到至少對(duì)匿名方法的委托引用符合垃圾收集條件時(shí)。
19.2.1 方法組轉(zhuǎn)換 像前面章節(jié)中描述過(guò)的那樣,一個(gè)匿名方法可以被隱式轉(zhuǎn)換為一個(gè)兼容的委托類型。c# 2.0允許對(duì)一組方法進(jìn)行相同的轉(zhuǎn)換,即所任何時(shí)候都可以省略一個(gè)委托的顯式實(shí)例化。例如,下面的語(yǔ)句: addbutton.click += new eventhandler(addclick); apply(a, new function(math.sin)); 還可以寫(xiě)做: addbutton.click += addclick; apply(a, math.sin); 當(dāng)使用短形式時(shí),編譯器可以自動(dòng)地推斷應(yīng)該實(shí)例化哪一個(gè)委托類型,不過(guò)除此之外的效果都和長(zhǎng)形式相同。
19.3 迭代器 c#中的foreach語(yǔ)句用于迭代一個(gè)可枚舉(enumerable)的集合中的元素。為了實(shí)現(xiàn)可枚舉,一個(gè)集合必須要有一個(gè)無(wú)參的、返回枚舉器(enumerator)的getenumerator方法。通常,枚舉器是很難實(shí)現(xiàn)的,因此簡(jiǎn)化枚舉器的任務(wù)意義重大。 迭代器(iterator)是一塊可以產(chǎn)生(yields)值的有序序列的語(yǔ)句塊。迭代器通過(guò)出現(xiàn)的一個(gè)或多個(gè)yield語(yǔ)句來(lái)區(qū)別于一般的語(yǔ)句塊: • yield return語(yǔ)句產(chǎn)生本次迭代的下一個(gè)值。 • yield break語(yǔ)句指出本次迭代完成。 只要一個(gè)函數(shù)成員的返回值是一個(gè)枚舉器接口(enumerator interfaces)或一個(gè)可枚舉接口(enumerable interfaces),我們就可以使用迭代器: • 所謂枚舉器借口是指system.collections.ienumerator和從system.collections.generic.ienumerator<t>構(gòu)造的類型。 • 所謂可枚舉接口是指system.collections.ienumerable和從system.collections.generic.ienumerable<t>構(gòu)造的類型。 理解迭代器并不是一種成員,而是實(shí)現(xiàn)一個(gè)功能成員是很重要的。一個(gè)通過(guò)迭代器實(shí)現(xiàn)的成員可以用一個(gè)或使用或不使用迭代器的成員覆蓋或重寫(xiě)。 下面的stack<t>類使用迭代器實(shí)現(xiàn)了它的getenumerator方法。其中的迭代器按照從頂端到底端的順序枚舉了棧中的元素。 using system.collections.generic; public class stack<t>: ienumerable<t> { t[] items; int count; public void push(t data) {...} public t pop() {...} public ienumerator<t> getenumerator() { for (int i = count – 1; i >= 0; --i) { yield return items; } } } getenumerator方法的出現(xiàn)使得stack<t>成為一個(gè)可枚舉類型,這允許stack<t>的實(shí)例使用foreach語(yǔ)句。下面的例子將值0至9壓入一個(gè)整數(shù)堆棧,然后使用foreach循環(huán)按照從頂端到底端的順序顯示每一個(gè)值。 using system; class test { static void main() { stack<int> stack = new stack<int>(); for (int i = 0; i < 10; i++) stack.push(i); foreach (int i in stack) console.write("{0} ", i); console.writeline(); } } 這個(gè)例子的輸出為: 9 8 7 6 5 4 3 2 1 0 語(yǔ)句隱式地調(diào)用了集合的無(wú)參的getenumerator方法來(lái)得到一個(gè)枚舉器。一個(gè)集合類中只能定義一個(gè)這樣的無(wú)參的getenumerator方法,不過(guò)通常可以通過(guò)很多途徑來(lái)實(shí)現(xiàn)枚舉,包括使用參數(shù)來(lái)控制枚舉。在這些情況下,一個(gè)集合可以使用迭代器來(lái)實(shí)現(xiàn)能夠返回可枚舉接口的屬性和方法。例如,stack<t>可以引入兩個(gè)新的屬性——ienumerable<t>類型的toptobottom和bottomtotop: using system.collections.generic; public class stack<t>: ienumerable<t> { t[] items; int count; public void push(t data) {...} public t pop() {...} public ienumerator<t> getenumerator() { for (int i = count – 1; i >= 0; --i) { yield return items; } } public ienumerable<t> toptobottom { get { return this; } } public ienumerable<t> bottomtotop { get { for (int i = 0; i < count; i++) { yield return items; } } } } toptobottom屬性的get訪問(wèn)器只返回this,因?yàn)槎褩1旧砭褪且粋€(gè)可枚舉類型。bottomtotop屬性使用c#迭代器返回了一個(gè)可枚舉接口。下面的例子顯示了如何使用這兩個(gè)屬性來(lái)以任意順序枚舉棧中的元素: using system; class test { static void main() { stack<int> stack = new stack<int>(); for (int i = 0; i < 10; i++) stack.push(i); foreach (int i in stack.toptobottom) console.write("{0} ", i); console.writeline(); foreach (int i in stack.bottomtotop) console.write("{0} ", i); console.writeline(); } } 當(dāng)然,這些屬性還可以用在foreach語(yǔ)句的外面。下面的例子將調(diào)用屬性的結(jié)果傳遞給一個(gè)獨(dú)立的print方法。這個(gè)例子還展示了一個(gè)迭代器被用作一個(gè)帶參的fromtoby方法的方法體: using system; using system.collections.generic; class test { static void print(ienumerable<int> collection) { foreach (int i in collection) console.write("{0} ", i); console.writeline(); } static ienumerable<int> fromtoby(int from, int to, int by) { for (int i = from; i <= to; i += by) { yield return i; } } static void main() { stack<int> stack = new stack<int>(); for (int i = 0; i < 10; i++) stack.push(i); print(stack.toptobottom); print(stack.bottomtotop); print(fromtoby(10, 20, 2)); } } 這個(gè)例子的輸出為: 9 8 7 6 5 4 3 2 1 0 0 1 2 3 4 5 6 7 8 9 10 12 14 16 18 20 泛型和非泛型的可枚舉接口都只有一個(gè)單獨(dú)的成員,一個(gè)無(wú)參的getenumerator方法,它返回一個(gè)枚舉器接口。一個(gè)可枚舉接口很像一個(gè)枚舉器工廠(enumerator factory)。每當(dāng)調(diào)用了一個(gè)正確地實(shí)現(xiàn)了可枚舉接口的類的getenumerator方法時(shí),都會(huì)產(chǎn)生一個(gè)獨(dú)立的枚舉器。 using system; using system.collections.generic; class test { static ienumerable<int> fromto(int from, int to) { while (from <= to) yield return from++; } static void main() { ienumerable<int> e = fromto(1, 10); foreach (int x in e) { foreach (int y in e) { console.write("{0,3} ", x * y); } console.writeline(); } } } 上面的代碼打印了一個(gè)從1到10的簡(jiǎn)單乘法表。注意fromto方法只調(diào)用了一次用來(lái)產(chǎn)生可枚舉接口e。而e.getenumerator()被調(diào)用了多次(通過(guò)foreach語(yǔ)句)來(lái)產(chǎn)生多個(gè)相同的枚舉器。這些枚舉器都封裝了fromto聲明中指定的代碼。注意,迭代其代碼改變了from參數(shù)。不過(guò),枚舉器是獨(dú)立的,因?yàn)閷?duì)于from參數(shù)和to參數(shù),每個(gè)枚舉器擁有它自己的一份拷貝。在實(shí)現(xiàn)可枚舉類和枚舉器類時(shí),枚舉器之間的過(guò)渡狀態(tài)(一個(gè)不穩(wěn)定狀態(tài))是必須消除的眾多細(xì)微瑕疵之一。c#中的迭代器的設(shè)計(jì)可以幫助消除這些問(wèn)題,并且可以用一種簡(jiǎn)單的本能的方式來(lái)實(shí)現(xiàn)健壯的可枚舉類和枚舉器類。
19.4 不完全類型 盡管在一個(gè)單獨(dú)的文件中維護(hù)一個(gè)類型的所有代碼是一項(xiàng)很好的編程實(shí)踐,但有些時(shí)候,當(dāng)一個(gè)類變得非常大,這就成了一種不切實(shí)際的約束。而且,程序員經(jīng)常使用代碼生成器來(lái)生成一個(gè)應(yīng)用程序的初始結(jié)構(gòu),然后修改產(chǎn)生的代碼。不幸的是,當(dāng)以后需要再次發(fā)布原代碼的時(shí)候,現(xiàn)存的修正會(huì)被重寫(xiě)。 不完全類型允許類、結(jié)構(gòu)和接口被分成多個(gè)小塊兒并存貯在不同的源文件中使其容易開(kāi)發(fā)和維護(hù)。另外,不完全類型可以分離機(jī)器產(chǎn)生的代碼和用戶書(shū)寫(xiě)的部分,這使得用工具來(lái)加強(qiáng)產(chǎn)生的代碼變得容易。 要在多個(gè)部分中定義一個(gè)類型的時(shí)候,我們使用一個(gè)新的修飾符——partial。下面的例子在兩個(gè)部分中實(shí)現(xiàn)了一個(gè)不完全類。這兩個(gè)部分可能在不同的源文件中,例如第一部分可能是機(jī)器通過(guò)數(shù)據(jù)庫(kù)影射工具產(chǎn)生的,而第二部分是手動(dòng)創(chuàng)作的: public partial class customer { private int id; private string name; private string address; private list<order> orders; public customer() { ... } } public partial class customer { public void submitorder(order order) { orders.add(order); } public bool hasoutstandingorders() { return orders.count > 0; } } 當(dāng)上面的兩部分編譯到一起時(shí),產(chǎn)生的代碼就好像這個(gè)類被寫(xiě)在一個(gè)單元中一樣: public class customer { private int id; private string name; private string address; private list<order> orders; public customer() { ... } public void submitorder(order order) { orders.add(order); } public bool hasoutstandingorders() { return orders.count > 0; } } 不完全類型的所有部分必須放到一起編譯,才能在編譯期間將它們合并。需要特別注意的是,不完全類型并不允許擴(kuò)展已編譯的類型。