|
接泛型四 20.6.5語法歧義 在§20.9.3和§20.9.4中簡單名字(simple-name)和成員訪問(member-access)對于表達(dá)式來說容易引起語法歧義。例如,語句 F(G<A,B>(7));
可以被解釋為對帶有兩個參數(shù)G<A和B>(7)的F的調(diào)用[1]。同樣,它還能被解釋為對帶有一個參數(shù)的F的調(diào)用,這是一個對帶有兩個類型實(shí)參和一個正式參數(shù)的泛型方法G的調(diào)用。 如果表達(dá)式可以被解析為兩種不同的有效方法,那么在“>”能被解析作為運(yùn)算符的所有或一部分時,或者作為一個類型實(shí)參列表,那么緊隨“>”之后的標(biāo)記將會被檢查。如果它是如下之一: { } ] > : ; , . ? 那么“>”被解析作為類型實(shí)參列表。否則“>”被解析作為一個運(yùn)算符。 20.6.6對委托使用泛型方法 委托的實(shí)例可通過引用一個泛型方法的聲明而創(chuàng)建。委托表達(dá)式確切的編譯時處理,包括引用泛型方法的委托創(chuàng)建表達(dá)式,這在§20.9.6中進(jìn)行了描述。 當(dāng)通過委托調(diào)用一個泛型方法時,所使用的類型實(shí)參將在委托實(shí)例化時被確定。類型實(shí)參可以通過類型實(shí)參列表顯式給定,或者通過類型推斷(§20.6.4)而確定。如果采用類型推斷,委托的參數(shù)類型將被用作推斷處理過程的實(shí)參類型。委托的返回類型不用于推斷。下面的例子展示了為一個委托實(shí)例化表達(dá)式提供類型實(shí)參的方法。 delegate int D(string s , int i) delegate int E(); class X { public static T F<T>(string s ,T t){…} public static T G<T>(){…} static void Main() { D d1 = new D(F<int>); //ok,類型實(shí)參被顯式給定 D d2 = new D(F); //ok,int作為類型實(shí)參而被推斷 E e1 = new E(G<int>); //ok,類型實(shí)參被顯式給定 E e2 = new E(G); //錯誤,不能從返回類型推斷 } } 在先前的例子中,非泛型委托類型使用泛型方法實(shí)例化。你也可以使用泛型方法創(chuàng)建一個構(gòu)造委托類型的實(shí)例。在所有情形下,當(dāng)委托實(shí)例被創(chuàng)建時,類型實(shí)參被給定或可以被推斷,但委托被調(diào)用時,可以不用提供類型實(shí)參列表(§15.3)。
20.6.7非泛型屬性、事件、索引器或運(yùn)算符 屬性、事件、索引器和運(yùn)算符他們自身可以沒有類型實(shí)參(盡管他們可以出現(xiàn)在泛型類中,并且可從一個封閉類中使用類型實(shí)參)。如果需要一個類似屬性泛型的構(gòu)件,取而代之的是你必須使用一個泛型方法。 20.7約束 泛型類型和方法聲明可以可選的指定類型參數(shù)約束,這通過在聲明中包含類型參數(shù)約束語句就可以做到。 type-parameter-constraints-clauses(類型參數(shù)約束語句:) type-parameter-constraints-clause(類型參數(shù)約束語句) type-parameter-constraints-clauses type-parameter-constraints-clause(類型參數(shù)約束語句 類型參數(shù)約束語句) type-parameter-constraints-clause:(類型參數(shù)約束語句:) where type-parameter : type-parameter-constraints(where 類型參數(shù):類型參數(shù)約束) type-parameter-constraints:(類型參數(shù)約束:) class-constraint(類約束) interface-constraints(接口約束) constructor-constraint(構(gòu)造函數(shù)約束) class-constraint , interface-constraints(類約束,接口約束) class-constraint , constructor-constraint(類約束,構(gòu)造函數(shù)約束) interface-constraints , constructor-constraint(接口約束,構(gòu)造函數(shù)約束) class-constraint , interface-constraints , constructor-constraint(類約束,接口約束,構(gòu)造函數(shù)約束) class-constraint:(類約束:) class-type(類類型) interface-constraint:(接口約束:) interface-constraint(接口約束) interface-constraints , interface-constraints(接口約束,接口約束) interface-constraints:(接口約束:) interface-type(接口類型) constructor-constraint:(構(gòu)造函數(shù)約束:) new ( ) 每個類型參數(shù)約束語句由標(biāo)志where 緊接著類型參數(shù)的名字,緊接著冒號和類型參數(shù)的約束列表。對每個類型參數(shù)只能有一個where 語句,但where語句可以以任何順序列出。與屬性訪問器中的get和set標(biāo)志相似,where 語句不是關(guān)鍵字。
在where語句中給定的約束列表可以以這個順序包含下列組件:一個單一的類約束、一個或多個接口約和構(gòu)造函數(shù)約束new ()。 如果約束是一個類類型或者接口類型,這個類型指定類型參數(shù)必須支持的每個類型實(shí)參的最小“基類型”。無論什么時候使用一個構(gòu)造類型或者泛型方法,在編譯時對于類型實(shí)參的約束建會被檢查。所提供的類型實(shí)參必須派生于或者實(shí)現(xiàn)那個類型參數(shù)個定的所有約束。 被指定作為類約束的類型必須遵循下面的規(guī)則。 該類型必須是一個類類型。 該類型必須是密封的(sealed)。 該類型不能是如下的類型:System.Array,System.Delegate,System.Enum,或者System.ValueType類型。 該類型不能是object。由于所有類型派生于object,如果容許的話這種約束將不會有什么作用。 至多,對于給定類型參數(shù)的約束可以是一個類類型。 作為接口約束而被指定的類型必須滿足如下的規(guī)則。 該類型必須是一個接口類型。 在一個給定的where語句中相同的類型不能被指定多次。 在很多情況下,約束可以包含任何關(guān)聯(lián)類型的類型參數(shù)或者方法聲明作為構(gòu)造類型的一部分,并且可以包括被聲明的類型,但約束不能是一個單一的類型參數(shù)。 被指定作為類型參數(shù)約束的任何類或者接口類型,作為泛型類型或者被聲明的方法,必須至少是可訪問的(§10.5.4)。 如果一個類型參數(shù)的where 語句包括new()形式的構(gòu)造函數(shù)約束,則使用new 運(yùn)算符創(chuàng)建該類型(§20.8.2)的實(shí)例是可能的。用于帶有一個構(gòu)造函數(shù)約束的類型參數(shù)的任何類型實(shí)參必須有一個無參的構(gòu)造函數(shù)(詳細(xì)情形參看§20.7)。 下面是可能約束的例子 interface IPrintable { void Print(); }
interface IComparable<T> { int CompareTo(T value); } interface IKeyProvider<T> { T GetKey(); } class Printer<T> where T:IPrintable{…} class SortedList<T> where T: IComparable<T>{…} class Dictionary<K,V> where K:IComparable<K> where: V: IPrintable,IKeyProvider<K>,new() { … } 下面的例子是一個錯誤,因?yàn)樗噲D直接使用一個類型參數(shù)作為約束。 class Extend<T , U> where U:T{…}//錯誤 約束的類型參數(shù)類型的值可以被用于訪問約束暗示的實(shí)例成員。在例子 interface IPrintable { void Print(); } class Printer<T> where T:IPrintable { void PrintOne(T x) { x.Pint(); } } IPrintable的方法可以在x上被直接調(diào)用,因?yàn)門被約束總是實(shí)現(xiàn)IPrintable。 20.7.1遵循約束 無論什么時候使用構(gòu)造類型或者引用泛型方法,所提供的類型實(shí)參都將針對聲明在泛型類型,或者方法中的類型參數(shù)約束作出檢查。對于每個where 語句,對應(yīng)于命名的類型參數(shù)的類型實(shí)參A將按如下每個約束作出檢查。
如果約束是一個類類型或者接口類型,讓C表示提供類型實(shí)參的約束,該類型實(shí)參將替代出現(xiàn)在約束中的任何類型參數(shù)。為了遵循約束,類型A必須按如下方式可別轉(zhuǎn)化為類型C: - 同一轉(zhuǎn)換(§6.1.1) - 隱式引用轉(zhuǎn)換(§6.1.4) - 裝箱轉(zhuǎn)換(§6.1.5) - 從類型參數(shù)A到C(§20.7.4)的隱式轉(zhuǎn)換。 如果約束是new(),類型實(shí)參A不能是abstract并,且必須有一個公有的無參的構(gòu)造函數(shù)。如果如下之一是真實(shí)的這將可以得到滿足。 - A是一個值類型(如§4.1.2中所描述的,所有值類型都有一個公有默認(rèn)構(gòu)造函數(shù))。 - A是一個非abstract類,并且 A包含一個無參公有構(gòu)造函數(shù)。 - A是一個非abstract類,并且有一個默認(rèn)構(gòu)造函數(shù)(§10.10.4)。 如果一個或多個類型參數(shù)的約束通過給定的類型實(shí)參不能滿足,將會出現(xiàn)編譯時錯誤。 因?yàn)轭愋蛥?shù)不被繼承,同樣約束也決不被繼承。在下面的例子中,D 必須在其類型參數(shù)T上指定約束,以滿足由基類B<T>所施加的約束。相反,類E不需要指定約束,因?yàn)閷τ谌魏蜹,List<T>實(shí)現(xiàn)了IEnumerable接口。 class B<T> where T: IEnumerable{…} class D<T>:B<T> where T:IEnumerable{…} class E<T>:B<List<T>>{…} 20.7.2 在類型參數(shù)上的成員查找 在由類型參數(shù)T給定的類型中,成員查找的結(jié)果取決于為T所指定的約束(如果有的話)。如果T沒有約束或者只有new ()約束,在T上的成員查找,像在object上的成員查找一樣,返回一組相同的成員。否則,成員查找的第一個階段,將考慮T所約束的每個類型的所有成員,結(jié)果將會被合并,然后隱藏成員將會從合并結(jié)果中刪除。
在泛型出現(xiàn)之前,成員查找總是返回在類中唯一聲明的一組成員,或者一組在接口中唯一聲明的成員, 也可能是object類型。在類型參數(shù)上的成員查找做出了一些改變。當(dāng)一個類型參數(shù)有一個類約束和一個或多個接口約束時,成員查找可以返回一組成員,這些成員有一些是在類中聲明的,還有一些是在接口中聲明的。下面的附加規(guī)則處理了這種情況。 在成員查找過程(§20.9.2)中,在除了object之外的類中聲明的成員隱藏了在接口中聲明的成員。 在方法和索引器的重載決策過程中,如果任何可用成員在一個不同于object的類中聲明,那么在接口中聲明的所有成員都將從被考慮的成員集合中刪除。 這些規(guī)則只有在將一個類約束和接口約束綁定到類型參數(shù)上時才有效。通俗的說法是,在一個類約束中定義的成員,對于在接口約束的成員來說總是首選。 20.7.3 類型參數(shù)和裝箱 當(dāng)一個結(jié)構(gòu)類型重寫繼承于System.Object(Equals , GetHashCode或ToString)的虛擬方法,通過結(jié)構(gòu)類型的實(shí)例調(diào)用虛擬方法將不會導(dǎo)致裝箱。即使當(dāng)結(jié)構(gòu)被用作一個類型參數(shù),并且調(diào)用通過類型參數(shù)類型的實(shí)例而發(fā)生,情況也是如此。例如 using System; struct Counter { int value; public override string ToString() { value++; return value.ToString(); } } class Program { static void Test<T>() where T:new() { T x = new T(); Console.WriteLine(x.ToString()); Console.WriteLine(x.ToString()); Console.WriteLine(x.ToString()); } static void Main() { Test<Counter>(); } }
程序的輸出如下 1 2 3 盡管推薦不要讓ToString帶有附加效果(side effect)[2],但這個例子說明了對于三次x.ToString()的調(diào)用不會發(fā)生裝箱。 當(dāng)在一個約束的類型參數(shù)上訪問一個成員時,裝箱決不會隱式地發(fā)生。例如,假定一個接口ICounter包含了一個方法Increment,它可以被用來修改一個值。如果ICounter被用作一個約束,Increment方法的實(shí)現(xiàn)將通過Increment在其上調(diào)用的變量的引用而被調(diào)用,這個變量不是一個裝箱拷貝。 using System; interface ICounter { void Increment(); } struct Counter:ICounter { int value; public override string ToString() { return value.ToString(); } void ICounter.Increment(){ value++; } } class Program { static void Test<T>() where T:new ,ICounter{ T x = new T(); Console.WriteLine(x); x.Increment(); //修改x` Console.WriteLine(x); ((ICounter)x).Increment(); //修改x的裝箱拷貝 Console.WriteLine(x); } static void Main() { Test<Counter>(); } }
對變量x的首次調(diào)用Increment修改了它的值。這與第二次調(diào)用Increment是不等價的,第二次修改的是x裝箱后的拷貝,因此程序的輸出如下 20.7.4包含類型參數(shù)的轉(zhuǎn)換 在類型參數(shù)T上允許的轉(zhuǎn)換,取決于為T所指定的約束。所有約束的或非約束的類型參數(shù),都可以有如下轉(zhuǎn)換。 從T到T的隱式同一轉(zhuǎn)換。 從T到object 的隱式轉(zhuǎn)換。在運(yùn)行時,如果T是一個值類型,這將通過一個裝箱轉(zhuǎn)換進(jìn)行。否則,它將作為一個隱式地引用轉(zhuǎn)換。 從object到T的隱式轉(zhuǎn)換。在運(yùn)行時,如果T是一個值類型,這將通過一個取消裝箱操作而進(jìn)行。否則它將作為一個顯式地引用轉(zhuǎn)換。 從T到任何接口類型的顯式轉(zhuǎn)換。在運(yùn)行時,如果T是一個值類型,這將通過一個裝箱轉(zhuǎn)換而進(jìn)行。否則,它將通過一個顯式地引用轉(zhuǎn)換而進(jìn)行。 從任何接口類型到T的隱式轉(zhuǎn)換。在運(yùn)行時,如果T是一個值類型,這將通過一個取消裝箱操作而進(jìn)行。否則,它將作為一個顯式引用轉(zhuǎn)換而進(jìn)行。 如果類型參數(shù)T指定一個接口I作為約束,將存在下面的附加轉(zhuǎn)換。 從T到I的隱式轉(zhuǎn)換,以及從T到I的任何基接口類型的轉(zhuǎn)換。在運(yùn)行時,如果T是一個值類型,這將作為一個裝箱轉(zhuǎn)換而進(jìn)行。否則,它將作為一個隱式地引用轉(zhuǎn)換而進(jìn)行。 如果類型參數(shù)T指定類型C作為約束,將存在下面的附加轉(zhuǎn)換: 從T到C的隱式引用轉(zhuǎn)換,從T到任何C從中派生的類,以及從T到任何從其實(shí)現(xiàn)的接口。 從C到T的顯式引用轉(zhuǎn)換,從C從中派生的類[3]到T,以及C實(shí)現(xiàn)的任何接口到T
如果存在從C 到A的隱式用戶定義轉(zhuǎn)換,從T到 A的隱式用戶定義轉(zhuǎn)換。 如果存在從A 到C的顯式用戶定義轉(zhuǎn)換,從A到 T的顯式用戶定義轉(zhuǎn)換。 從null類型到T的隱式引用轉(zhuǎn)換 一個帶有元素類型的數(shù)組類型T具有object和System.Array之間的相互轉(zhuǎn)換(§6.1.4,§6.2.3)。如果T有作為約束而指定的類類型,將有如下附加規(guī)則 從帶有元素類型T的數(shù)組類型AT到帶有元素類型U的數(shù)組類型AU的隱式引用轉(zhuǎn)換,并且如果下列二者成立的話,將存在從AU到AT顯式引用轉(zhuǎn)換: - AT和AU 有相同數(shù)量的維數(shù)。 - U是這些之一:C,C從中派生的類型,C所實(shí)現(xiàn)的接口,作為在T上的約束而指定的接口I,或I的基接口。 先前的規(guī)則不允許從非約束類型參數(shù)到非接口類型的直接隱式轉(zhuǎn)換,這可能有點(diǎn)奇怪。其原因是為了防止混淆,并且使得這種轉(zhuǎn)換的語義更明確。例如,考慮下面的聲明。 class X<T> { public static long F(T t){ return (long)t; // ok,允許轉(zhuǎn)換 } } 如果t到int的直接顯式轉(zhuǎn)換是允許的,你可能很容易以為X<int>.F(7)將返回7L。但實(shí)際不是,因?yàn)闃?biāo)準(zhǔn)的數(shù)值轉(zhuǎn)換只有在類型在編譯時是已知的時候才被考慮。為了使語義更清楚,先前的例子必須按如下形式編寫。 class X<T> { public static long F(T t) { return (long)(object)t; //ok;允許轉(zhuǎn)換 } }
--------------------------------------------------------------------------------
[1] 這種情況下“>”被解釋為大于運(yùn)算符。 [2] 在程序中重寫ToString時,一般不推薦添加這種類似的計算邏輯,因?yàn)樗倪@種結(jié)果變化不易控制,增加了調(diào)試程序的復(fù)雜性。 [3] C的基類或其基類的基類等。
|