重温CLR(八 ) 泛型
熟悉面向對象編程的開發(fā)人員都深諳面向對象的好處,其中一個好處是代碼重用,它極大提高了開發(fā)效率。也就是說,可以派生出一個類,讓他繼承基類的所有能力。派生類只需要重寫虛方法,或添加一些新方法,就可定制派生類的行為,使之滿足開發(fā)人員的需求。泛型(generic)是clr和編程語言提供的一種特殊機制,它支持另一種形式的代碼重用,即“算法重用”。
?????? 簡單地說,開發(fā)人員先定義好算法,比如排序、搜索、交換、比較或者轉換等。但是,定義算法的開發(fā)人員并不設定該算法要操作什么數(shù)據(jù)類型。然后,另一個開發(fā)人員只要制定了算法要操作的具體數(shù)據(jù)類型,就可以使用算法了。
?????? 大多數(shù)算法都封裝在一個類型中,clr允許創(chuàng)建泛型引用類型和泛型值類型,但不允許創(chuàng)建泛型枚舉類型。此外,clr還允許創(chuàng)建泛型接口和泛型委托。方法偶爾也封裝有用的算法,所以clr允許在引用類型、值類型或接口中定義泛型方法。
泛型有兩種表現(xiàn)形式:泛型類型和泛型方法。
泛型類型:大多數(shù)算法都封裝在一個類型中,CLR允許創(chuàng)建泛型引用類型和泛型值類型,但不允許創(chuàng)建泛型枚舉類型。除此之外,CLR還允許創(chuàng)建泛型接口和泛型委托。
泛型方法:方法偶爾也封裝有用的算法,所以CLR允許引用類型、值類型或接口中定義泛型方法。
兩者都是表示API的基本方法(不管是指一個泛型方法還是一個完整的泛型類型),以致平時期望出現(xiàn)一個普通類型的地方出現(xiàn)一個類型參數(shù)。比如,List<T>,在類名之后添加一個<T>,表明它操作的是一個未指定的數(shù)據(jù)類型。定義泛型類型和方法時,它為類型指定的任何變量(比如 T)都稱為類型參數(shù)(type parameter)。T代表一個變量名,在源代碼中能夠使用一個數(shù)據(jù)類型的任何位置 ,都能使用T。
類型參數(shù)是真實類型的占位符。在泛型聲明中,類型參數(shù)要放在一堆尖括號內,并以逗號分隔。所以,在Dictionary<TKey, TValue>中,類型參數(shù)是TKey和TValue。使用泛型類型或方法時,要使用真實的類型代替。這些真實的類型稱為類型實參(type argument)。
泛型為開發(fā)人員提供了以下優(yōu)勢:
1) 源代碼保護
?????? 使用一個泛型算法的開發(fā)人員不需要訪問算法的源代碼。然而,使用C++模板的泛型技術時,算法的源代碼必須提供給準備使用算法的用戶。
2) 類型安全
將一個泛型算法應用于一個具體的類型時,編譯器和CLR能理解開發(fā)人員的意圖,并保證只有與制定數(shù)據(jù)類型兼容的對象才能隨同算法使用。
3) 更清晰的代碼
由于編譯器強制類型安全性,所以減少了源代碼中必須進行的轉型次數(shù)。
4) 更佳的性能
在有泛型之前,要想定義一個常規(guī)化的算法,它的所有成員都要定義成操作Object數(shù)據(jù)類型。這其中就要有裝箱和拆箱之間的性能損失。由于現(xiàn)在能創(chuàng)建一個泛型算法來操作一個具體的值類型,所以值類型的實例能以傳值的方式傳遞,CLR不再需要只需任何裝箱操作。由于不再需要轉型,所以CLR不必檢查嘗試一次轉型操作是否類型安全,同樣提高了代碼的允許速度。
fcl中的泛型
泛型最明顯的應用就是集合類。FCL已經(jīng)定義了幾個泛型集合類。其中大多數(shù)類能在Sysytem.Collections.Generic和System.Collections.ObjectModel命名空間中。要使用線程安全的泛型集合類,可以去System.Collections.Concurrent命名空間尋找。
Microsoft建議開發(fā)人員使用泛型集合類,并基于幾個方面的原因,不鼓勵使用非泛型集合類(arrayList之類)。首先,非泛型無法獲得類型安全性、更清晰的代碼和更佳的性能。其次,泛型具有更好的對象模型。
集合類實現(xiàn)了許多接口,放入集合中的對象也可能實現(xiàn)了接口,集合類可利用這些接口執(zhí)行像排序這樣的操作。FCL內建了許多泛型接口定義,所以在使用接口時,也能體會到泛型帶來的好處。常用的接口包含在Sysytem.Collections.Generic命名空間中。
System.Array類(即所有數(shù)組的基類)提供了大量靜態(tài)泛型方法,比如,AsReadonly、FindAll、Find、FindIndex等。
?
泛型基礎結構
?????? 泛型在clr2.0中加入。為了在clr加入泛型,許多人花費了大量時間來完成這個任務,比如以下
1 創(chuàng)建新的il命令,使之能夠識別類型實參
2 修改現(xiàn)有元數(shù)據(jù)表的格式,以便表示具有泛型參數(shù)的類型和方法
3 修改編程語言以支持新語法
4 修改編譯器,使之能生成新的il指令和修改的元數(shù)據(jù)格式
5 創(chuàng)建新的反射成員,是開發(fā)人員能查詢類型和成員
?????? 現(xiàn)在,讓我們一起討論clr內部如何處理泛型。
開放類型和封閉類型
?????? 我們討論了clr如何為應用程序使用的各種類型創(chuàng)建稱為類型對象(type object)的內部數(shù)據(jù)結構。具有泛型類型參數(shù)的類型仍然是類型,clr同樣會為它創(chuàng)建內部的類型對象。這一點適合引用類型(類)、值類型(結構)、接口類型和委托類型。然而,具有泛型類型參數(shù)的類型稱為開放類型,clr禁止構造開放類型的任何實例。這類似于clr禁止構造接口類型的實例。
?????? 代碼引用泛型類型時可指定一組泛型類型實參。為所有類型參數(shù)都傳遞了實際的數(shù)據(jù)類型,類型就稱為封閉類型。clr允許構造封閉類型的實例。然而,代碼引用泛型類型的時候,可能留下一些泛型類型實參未指定。這會在clr中創(chuàng)建新的開放類型對象,而且不能創(chuàng)建該類型的實例。如下例子
// A partially specified open type internal sealed class DictionaryStringKey<TValue> :Dictionary<String, TValue> { } class Program {private static void Main(string[] args){Object o = null;// Dictionary<,> 是一個開放類型,有兩個類型參數(shù)Type t = typeof(Dictionary<,>);// 嘗試創(chuàng)建該類型的一個實例 (失敗)o = CreateInstance(t);Console.WriteLine();// DictionaryStringKey<> 是一個開放類型,有一個類型參數(shù)t = typeof(DictionaryStringKey<>);// 嘗試創(chuàng)建該類型的一個實例 (失敗)o = CreateInstance(t);Console.WriteLine();// DictionaryStringKey<Guid> 是一個封閉類型t = typeof(DictionaryStringKey<Guid>);// 嘗試創(chuàng)建該類型的一個實例 (成功)o = CreateInstance(t);// Prove it actually workedConsole.WriteLine("Object type=" + o.GetType());Console.ReadKey();}private static Object CreateInstance(Type t){Object o = null;try{o = Activator.CreateInstance(t);Console.Write("已創(chuàng)建 {0} 的實例", t.ToString());}catch (ArgumentException e){Console.WriteLine(e.Message);}return o;} }還要注意的是,CLR會在類型對象內部分配類型的靜態(tài)字段。因此,每個封閉類型都有自己的靜態(tài)字段。換言之,假如List<T>定義了任何靜態(tài)字段,這些字段不會在一個List<DataTime>和List<String>之間共享;每個封閉類型對象都有它自己的靜態(tài)字段。另外,假如一個泛型類型定義了一個靜態(tài)構造器,那么針對每個封閉類型,這個構造器都會執(zhí)行一次。在泛型類型上定義一個靜態(tài)構造器的目的是保證傳遞的類型參數(shù)滿足特定的條件。例如,如果希望一個泛型類型值用于處理枚舉類型,可以如下定義:
internal sealed calss GenericTypeThatReqiresAnEnum<T> {static GenericTypeThatReqiresAnEnum() {if ( !typeof (T).IsEnum) {throw new ArgumentException("T must be an enumerated type")}} }CLR提供了一個名為"約束"(constraint)的功能,可利用它更好地定義一個泛型類型來指出哪個類型實參是有效的。
泛型類型和繼承
?????? 泛型類型仍然是類型,所以它能從其他任何類型派生。使用一個泛型類型并指定類型實參時,實際上是在CLR中定義一個新的類型對象,新的類型對象是從派生該泛型類型的那個類型派生的。也就是說,由于List<T>是從Object派生的,那么List<String>和List<Guid>也是從Object派生的。
?????? 類似地,由于DictionaryStringKey<TValue>從Dictionary<String,TValue>派生,所以DictionaryStringKey<Guid>也從Dictionary<String, Guid >派生。指定類型實參不影響繼承層次結構。
?????? 定義每個節(jié)點為具體數(shù)據(jù)類型的鏈表,比較好的辦法是定義非泛型Node基類,再定義泛型TypedNode類(用node類作為基類)。這樣每個節(jié)點都是一種具體的數(shù)據(jù)類型,同時獲得編譯時的類型安全性,并防止值類型裝箱。
class Node {protected Node m_next;public Node(Node next){m_next = next;} } internal sealed class TypedNode<T>:Node {public T m_data;public TypedNode(T data):this(data,null){}public TypedNode(T data,Node next):base(next){m_data = data;}public override string ToString(){return m_data + (m_next != null ? m_next.ToString() : string.Empty);} }?
泛型類型同一性
?????? 泛型語法有時會將開發(fā)人員弄糊涂,因為源代碼中可能散布著大量“<”和” >”符號,這有損可讀性。為了對語法進行增強,有的開發(fā)人員定義了一個新的非泛型類型,它從一個泛型類型派生,并制定了所有類型實參。
List<DateTime> dt = new List<DateTime>();
一些開發(fā)人員可能首先定義下面這樣的一個類:
internal sealed class DateTimeList : List<DataTime> {//這里無需放任何代碼! }然后就可以簡化創(chuàng)建列表的代碼
DateTimeList dt = new DateTimeList ();這樣做表面上是方便了,但是絕對不要單純出于增強源代碼的易讀性類這樣定義一個新類。這樣會喪失類型同一性(identity)和相等性(equivalence)。如下:
Boolean sameType = (typeof(List<DateTime>) == (typeof(DateTimeList));上述代碼運行時,sameType會初始化為false,因為比較的是兩個不同類型的對象。也就是說,假如一個方法的原型接受一個DateTimeList,那么不能將一個List<DateTime>傳給它。然而,如果方法的原型接受一個List<DateTime>,那么可以將一個DateTimeList傳給它,因為DateTimeList是從List<DateTime>派生的。
幸好,C#提供一種方式,允許使用簡化的語法來引用一個泛型封閉類型,同時不會影響類的相等性——使用using指令。比如:
using DateTimeList = System.Collections.Generic.List<System.DateTime>;
?????? using指令實際定義的是名為DateTimeList的符號。
現(xiàn)在只想下面這行代碼時,sameType會初始化為true:
Boolean sameType = (type(List<DateTime>) == (ypeof(DateTimeList));還有,可以使用C#的隱式類型局部變量功能,讓編譯器根據(jù)表達式的類型來推斷一個方法的局部變量的類型。
代碼爆炸
使用泛型類型參數(shù)的一個方法在進行JIT編譯時,CLR獲取方法的IL,用指定的類型實參進行替換,然后創(chuàng)建恰當?shù)谋镜卮a。然而,這樣做有一個缺點:CLR要為每種不同的方法/類型組合生成本地代碼。我們將這個現(xiàn)象稱為"代碼爆炸"。它可能造成引用程序集的顯著增大,從而影響性能。
CLR內建了一些優(yōu)化措施,能緩解代碼爆炸。首先,假如為一個特定的類型實參調用了一個方法,以后再次使用相同的類型實參來調用這個方法,CLR只會為這個方法/類型組合編譯一次。所以,如果一個程序集使用List<DateTime>,一個完全不同的程序集也使用List<DateTime>,CLR只會為List<DateTime>編譯一次方法。
?????? CLR還提供了一個優(yōu)化措施,它認為所有引用類型實參都是完全相同的,所以代碼能夠共享。例如,clr為List<String>的方法編譯的代碼可直接用于List<Stream>的方法,因為string和stream均為引用類型。事實上,對于任何引用類型,都會使用相同的代碼。clr之所以能執(zhí)行這個優(yōu)化,是因為所有引用類型的實參或變量實際只是指向堆上對象的指針,而所有對象執(zhí)政都以相同方式操作。
但是,假如某個類型實參是值類型,CLR就必須專門為那個值類型生成本地代碼。因為值類型的大小不定。即使類型、大小相同,CLR仍然無法共享代碼,可能需要用不同的本地CPU指令操作這些值
泛型接口
?????? 顯然,泛型的主要作用是定義泛型的引用類型和值類型。然而,對泛型接口的支持對clr來說也很重要。沒有泛型接口,每次用非泛型接口(如IComparable)來操縱值類型都會發(fā)生裝箱,而且會時區(qū)編譯時的類型安全性。這將嚴重制約泛型類型的應用方位。因此clr提供了對泛型接口的的和支持。引用類型或值類型可指定類型實參實現(xiàn)泛型接口。也可保持類型實參的未指定狀態(tài)來實現(xiàn)泛型接口。
以下是泛型接口定義是FCL的一部分:
public interface IEnumerator<T> : IDisposable, IEnumerator{T Current { get; } }下面的示例類型實現(xiàn)上述泛型接口,而且指定了類型實參。
internal sealed class Triangle : IEnumerator<Point> {private Point[] m_Vertice;public Point Current { get { ... } } }下面實現(xiàn)了相同的泛型接口,但保持類型實參的未指定狀態(tài):
internal sealed class ArrayEnumerator<T> : IEnumerator<T> {private T[] m_Vertice;public TCurrent { get { ... } } }?????? 注意,arrayEnumerator對象可枚舉一組T對象。還要注意,current屬性現(xiàn)在具有未指定的數(shù)據(jù)類型T。
泛型委托
?????? CLR支持泛型委托,目的還是保證任何類型的對象都能以類型安全的方式傳給回調方法。此外,泛型委托允許值類型實例在傳給回調方法時不進行任何裝箱。委托實際只是提供了4個方法的一個類定義。4個方法包括一個構造器、一個Invoke方法,一個BeginInvoke方法和一個EndInvoke方法。如果定義的委托類型制定了類型參數(shù),編譯器會定義委托類的方法,用指定的類型參數(shù)替換方法的參數(shù)類型和返回值類型。
例如,假定向下面這樣定義一個泛型委托:
?public delegate TReturn CallMe<TReturn, TKey, TValue>(TKey key, TValue value);
編譯器會將它轉化成一個類,該類在邏輯上可以這樣表示:
public sealed class CallMe<TReturn, TKey, TValue> : MulticastDelegate {public CallMe(Object object, IntPtr method);public virtual TReturn Invoke(TKey key, TValue value);public virtual IAsycResult BeginInvoke(TKey key, TValue value, AsyncCallback callback, Object object);public virtual TReturn EndInvoke(IAsycResult result); }建議盡量使用在FCL中預定義的泛型Action和Func委托。
委托和接口的逆變和協(xié)變泛型類型實參
?????? 委托的每個泛型類型參數(shù)都可標記為協(xié)變量或逆變量。利用這個功能,可將泛型委托類型的變量轉換為相同的委托類型(但泛型參數(shù)類型不同)。泛型類型參數(shù)可以是以下任何一種形式。
1 不變量(invariant) ?意味著泛型類型參數(shù)不能更改。到目前為止,你在本質看到的全是不變量形式的泛型類型參數(shù)。
2 逆變量(contravariant) 意味著泛型類型參數(shù)可以從一個類型更改為它的某個派生類。在c#使用in 關鍵字標記逆變量形式的泛型類型參數(shù)。逆變量泛型類型參數(shù)只出現(xiàn)在輸入位置,比如作為方法的參數(shù)。
3 協(xié)變量(covariant) 意味著泛型類型參數(shù)可以從一個類更改為它的某個基類,c#使用out關鍵字標記協(xié)變量是行的泛型類型參數(shù)。協(xié)變量泛型參數(shù)只能出現(xiàn)在輸出位置,比如作為方法的返回類型。
例如,現(xiàn)在存在以下委托類型定義(它在FCL中是存在的)
public delegate TResult Func<in T, Out TResult>(T arg);
其中,泛型類型參數(shù)T用in關鍵字標記,這使它成為一個逆變量;泛型類型參數(shù)TResulr則用out關鍵字標記,這是它成為一個協(xié)變量。
所以,如果像下面這樣聲明一個變量:
Func<Object,ArgumenException> fn1 = null;
就可以將它轉型為另一個泛型類型參數(shù)不同的Func類型:
Func<String,Exception> fn2 = fn1;??? //不需要顯示轉型
Exception e = fn("");
使用要獲取泛型參數(shù)和返回值的委托時,建議盡量為逆變性和協(xié)變性指定in和out關鍵字。這樣做不會有不良反應,并使你的委托能在更多的情形中使用。
和委托相似,具有泛型類型參數(shù)的接口也可將它的類型參數(shù)標記為逆變量和協(xié)變量。比如:
public interface IEnumerator<out T> : IEnumerator {Boolean MoveNext();T Current{ get; } }由于T是逆變量,所以以下代碼可以順利編譯:
//這個方法接受任意引用類型的一個IEnumerable Int32 Count(IEnumerable<Object> collection) { ... } //以下調用向Count傳遞一個IEnumerable<String> Int32 c = Count(new[] { "Grant" });泛型方法
?????? 定義泛型類、結構或接口時,這些類型中定義的任何方法都可引用由類型指定的一個類型參數(shù)。類型參數(shù)可以作為方法的參數(shù),作為方法的返回值,或者作為方法內部定義的一個局部變量來使用。然而,CLR還允許方法指定它獨有的類型參數(shù)。這些類型參數(shù)可用于參數(shù)、返回值或者局部變量的類型使用。
在下面的例子中,一個類型定義了一個類型參數(shù),一個方法則定義了它自己的專用類型參數(shù):
internal sealed class GenericType<T> {privete T m_value;public GenericType(T value) { m_value = value; }public TOutput Converter<TOutput>() {TOutput resulr= (TOurput) Convert.ChangeType(m_value,typeof(TOutput));return result;} }在這個例子中,GenericType類定義了類型參數(shù)(T),Converter方法也定義了自己的類型參數(shù)(TOutput)。這樣的GenericType可以處理任意類型。Converter方法能將m_value字段引用的對象轉換成任意類型—具體取決于調用時傳遞的類型實參是什么。泛型方法的存在,為開發(fā)人員提供了極大的靈活性。
?????? 泛型方法的一個很好的例子是swap方法:
private static void Swap<T>(ref T o1,ref T o2){T temp =o1;o1=o2;o2=temp; }泛型方法和類型推斷
c#泛型語法因為涉及大量<>符號,所以開發(fā)人員很容易被弄得暈頭轉向。為了改進代碼的創(chuàng)建,同事增強可讀性和維護性,C#編譯器支持在調用一個泛型方法時進行類型推斷(type inference)。這意味著編譯器會在調用一個泛型方法時自動判斷出要使用的類型。
private static void CallingSwapUsingInference() {Int32 n1 = 1, n2 = 2;Swap(ref n1, ref n2); //調用Swap<Int32>String s1 = "A";Object s2 = "B";Swap(ref s1, ref s2); //錯誤,不能推斷類型 }執(zhí)行類型推斷時,C#使用變量的數(shù)據(jù)類型,而不是由變量引用的對象的實際類型。所以第二個swap調用中,c#發(fā)現(xiàn)s1是string,而s2是object(即使它恰好引用一個string)。由于s1和s2是不同數(shù)據(jù)類型的變量,編譯器拿不準要為swap傳遞什么類型實參,所以會報錯。
?????? 類型可定義多個方法,讓其中一個方法接收具體數(shù)據(jù)類型,讓另一個接收泛型類型參數(shù),如下例所示
private static void Display(string s){console.writeline(s) } private static void Display<T>(T o){Display(o.tostring()); }下面展示了display方法的一些調用方式
display(“jeff”); //調用Display(string s) display(123); //調用Display<T>(T o) display<string>(“asdasd”); //調用Display<T>(T o)?????? 在第一個調用中,編譯器可調用接收string參數(shù)的display方法,也可調用泛型display方法。但c#編譯器的策略是先考慮較明確的匹配,再考慮泛型匹配。對于第二個調用,編譯器不能調用接收string參數(shù)的非泛型方法,所以必須調用泛型方法。
?????? 對于第三個調用,明確制定了泛型類型實參string。這告訴編譯器不要嘗試推斷類型實參。相反,應使用顯式指定的類型實參。這個例子中,編譯器會假定我想調用泛型方法,所以會調用泛型方法。
泛型和其他成員
?????? 在c#中,屬性、索引器、事件、操作符方法、構造器和終結器本身不能有類型參數(shù)。但它們能在泛型類型中定義,而且這些成員的代碼能使用類型的類型參數(shù)。
?????? c#之所以不允許這些成員指定自己的泛型類型參數(shù),是因為Microsoft C#團隊認為開發(fā)人員很少需要將這些成員作為泛型使用。除此之外,為這些成員添加泛型支持的代價是相當高的,因為必須為語言設計足夠的語法。
?
可驗證性和約束
編譯泛型代碼時,c# 會進行分析,確保代碼適用于當前已有或將來可能定義的任何類型。看看下面方法
private static Boolean MethodTakingAnyType<T>(T o) {T temp = o;Console.WriteLine(o.ToString());bool b = temp.Equals(o);return b; }?????? 這個方法聲明了T類型的臨時變量(temp)。然后,方法執(zhí)行兩次變量賦值和幾次方法調用。這個方法適用于任何類型。無論T是引用類型,值類型或枚舉類型,還是接口或委托類型,它都能工作。
在看看下面方法
public static T Min<T>(T o1, T o2) {if (o1.CompareTo(o2)<0) return o1;return o2; }?????? min方法試圖使用o1變量來調用CompareTo方法。但是,許多類型都沒有提供CompareTo方法,所以c#編譯器不能編譯上述代碼,它不能保證這個方法適用于所有類型。
?????? 所以從表面上看,使用泛型似乎做不了太多事情。只能聲明泛型類型的變量,執(zhí)行變量賦值,再調用Object定義的方法,如此而已!顯然,加入泛型只能這么用,可以說它幾乎沒有任何用。幸好,編譯器和clr支持稱為約束的機制,可通過它使泛型變得真正有用!
?????? 約束的作用是限制能指定成泛型實參的類型數(shù)量。通過限制類型的數(shù)量,可以對那些類型執(zhí)行更多操作。以下以下還是新版本的min方法,他指定了一個約束。
public static T Min<T>(T o1, T o2) where T : IComparable<T> {if (o1.CompareTo(o2)<0) return o1;return o2;}?????? c#的wheer關鍵字告訴編譯器,為T指定的任何類型都必須實現(xiàn)同類型(T)的泛型IComparable接口。有了這個約束,就可以在方法中調用CompareTo,因為已知IComparable<T>接口定義了CompareTo。
?????? 現(xiàn)在,當代碼引用泛型類型或方法時,編譯器要負責保證類型實參復合指定的約束,如果不符合約束,編譯器會報錯。
約束可應用于一個泛型類型的類型參數(shù),也可應用于一個泛型方法的類型參數(shù)(就像Min所展示的)。CLR不允許基于類型參數(shù)名稱或約束來進行重載;只能基于元數(shù)(類型參數(shù)的個數(shù))對類型或方法進行重載。下例對此進行了演示
internal sealed class AType { } internal sealed class AType<T> { } internal sealed class AType<T1, T2> { }// 錯誤: 與沒有約束的 AType<T> 起沖突 internal sealed class AType<T> where T : IComparable<T> { }// 錯誤: 與 AType<T1, T2> 起沖突 internal sealed class AType<T3, T4> { }internal sealed class AnotherType {// 可以定義一下方法,參數(shù)個數(shù)不同:private static void M() { }private static void M<T>() { }private static void M<T1, T2>() { }// 錯誤: 與沒有約束的 M<T> 起沖突private static void M<T>() where T : IComparable<T> { }// 錯誤: 與 M<T1, T2> 起沖突private static void M<T3, T4>() { } }重寫虛泛型方法時,重寫的方法必須指定相同數(shù)量的類型參數(shù),而且這些類型參數(shù)會繼承在基類方法上指定的約束。事實上,根本不允許為重寫方法的類型參數(shù)指定任何約束。但是,類型參數(shù)的名稱是可以改變的。類似的,實現(xiàn)一個接口方法時,方法必須指定與接口方法等量的類型參數(shù),這些類型參數(shù)將繼承由接口的方法在它們前面指定的約束。下例使用虛方法演示了這一規(guī)則:
internal class Base {public virtual void M<T1, T2>()where T1 : structwhere T2 : class {} } internal sealed class Derived : Base {public override void M<T3, T4>()where T3 : structwhere T4 : class {} }?????? 試圖編譯上述代碼,編譯器會報告以下錯誤:
error CS0460:重寫和顯示接口實現(xiàn)方法的約束是從基方法繼承的,因此不能直接指定這些約束。
?????? 注釋掉子類的約束,即可正常編譯。下面討論編譯器\clr允許向類型參數(shù)應用的各種約束。可用一個主要約束、一個次要約束以及一個構造器約束來約束類型參數(shù)。
主要約束
?????? 類型參數(shù)可以指定零個或者一個主要約束。主要約束可以是代表未密封類的一個引用類型。不能指定以下特殊引用類型:System.Object,System.Array,System.Delagate,System.MulticastDelegate,System.ValueType,System.Enum和System.Void。
?????? 指定引用類型約束時,相當于向編譯器承諾:一個指定的類型實參要么是與約束類型相同的類型,要么是從約束類型派生的類型。例如以下泛型類:
internal static class PrimaryConstraintOfStream<T> where T : Stream {public static void M(T stream) {stream.Close(); // OK } }?????? 在這個類定義中,類型參數(shù)t設置了主要約束Stream(在system.IO命名空間中定義)。這就告訴編譯器,使用primaryConstraintOfStream的代碼在指定類型實參中,必須指定stream或者從stream派生的類型。如果類型參數(shù)沒有指定主要約束,就默認為system.object。
?????? 有兩個特殊的主要約束:class和struct。其中,class約束向編譯器承諾類型實參是引用類型。任何類類型、接口類型、委托類型或者數(shù)組類型都滿足這個約束。例如以下泛型類
internal static class PrimaryConstraintOfClass<T> where T : class {public static void M() {T temp = null; // 允許,T為引用類型 } }?????? 在這個例子中,將temp設為null是合法的,因為T已知是引用類型,而所有引用類型的變量都能設為null。不對T進行約束,上述代碼就通不過編譯,因為T可能是值類型,而值類型的變量不能設為null。
?????? struct約束向編譯器承諾類型實參是值類型。包括枚舉在內的任何值類型都滿足這個約束。但編譯器和clr將任何system.nullable<T>值類型視為特殊類型,不滿足這個struct約束。原因是nullable<T>類型將它的類型參數(shù)約束為struct,而clr希望禁止向nullable< nullable<T>> 這樣的遞歸類型。
internal static class PrimaryConstraintOfStruct<T> where T : struct {public static T Factory() {// 允許,因為值類型都有一個隱式無參構造器return new T();} }?????? 這個例子中的new T()是合法的,因為T已知是值類型,而所有值類型都隱式地有一個公共無參構造器。如果T不約束,約束為引用類型,或者約束為class,上述代碼將無法通過編譯,因為有的引用類型沒有公共無參構造器。
次要約束
?????? 類型參數(shù)可以指定零個或者多個次要約束,次要約束代表接口類型。這種約束向編譯器承諾類型實參實現(xiàn)了接口。由于能指定多個接口約束,所以類型實參必須實現(xiàn)了所有接口約束(以及主要約束,如果有的話)。
?????? 還有一種次要約束稱為類型參數(shù)約束,有時也成為裸類型約束。這種約束用的比接口約束少得多。它允許一個泛型類型或方法規(guī)定:指定的類型實參要么就是約束的類型,要么是約束的類型的派生類。一個類型參數(shù)可以指定零個或多個類型參數(shù)約束。
?
private static List<TBase> ConvertIList<T, TBase>(IList<T> list)where T : TBase {List<TBase> baseList = new List<TBase>(list.Count);for (Int32 index = 0; index < list.Count; index++){baseList.Add(list[index]);}return baseList; }?????? ConvertIList方法制定了兩個類型參數(shù),其中T參數(shù)由TBase類型參數(shù)約束。意味著不管為T指定書目類型實參,都必須兼容與為TBase指定的類型實參。下面這個方法演示了對ConvertIList的合法調用和非法調用:
private static void CallingConvertIList() {//構造并初始化一個List<String>(它實現(xiàn)了IList<String>)IList<String> ls = new List<String>();ls.Add("A String");// 將IList<String>轉換成IList<Object>IList<Object> lo = ConvertIList<String, Object>(ls);// 將IList<String>轉換成IList<IComparable>IList<IComparable> lc = ConvertIList<String, IComparable>(ls);// 將IList<String>轉換成IList<IComparable<String>>IList<IComparable<String>> lcs =ConvertIList<String, IComparable<String>>(ls);// 將IList<String>轉換成IList<Exception>//IList<Exception> le = ConvertIList<String, Exception>(ls); // 錯誤 }構造器約束
類型參數(shù)可以指定零個或者一個構造器約束。它向編譯器承諾類型實參是實現(xiàn)了公共無參構造器的非抽象類型。注意,如果同時使用構造器約束和struct約束,c#編譯器會認為這是一個錯誤,因為這是多余的;所有值類型都隱式提供了公共無參構造器。
internal sealed class ConstructorConstraint<T> where T : new() {public static T Factory(){// 允許,因為值類型都有隱式無參構造器// 而約束要求任何引用類型也要有一個無參構造器return new T();} }?????? 這個例子中的new T()是合法的,因為已知t是擁有公共無參構造器的類型。對所有值類型來說,這一點肯定成立。對于作為類型實參指定的任何引用類型,這一點也成立,因為構造器約束要求它必須成立。
?????? 開發(fā)人員有時想為類型參數(shù)指定一個構造器約束,并指定構造器要獲取多個參數(shù)。目前,clr(以及c#編譯器)只支持無參構造器。Microsoft認為這已經(jīng)能滿足幾乎所有情況,我對此也表示同意。
其他可驗證性問題
?????? 本節(jié)剩下部分將討論幾個特殊的代碼構造。由于可驗證性問題,這些代碼構造在和泛型共同使用時,可能產(chǎn)生不可預期的行為。另外,還討論了如何利用約束使代碼重新變得可以驗證。
1) 泛型類型變量的轉型
將一個泛型類型的變量轉型為另一個類型是非法的,除非將其轉型為與一個約束兼容的類型:
private void CastingAGenericTypeVariable1<T>(T obj) {Int32 x = (Int32)obj; // 錯誤String s = (String)obj; // 錯誤 }上述兩行錯誤是因為T可以是任何任何類型,無法保證成功。為了修改上述代碼使其能通過編譯,可以先轉型為object:
private void CastingAGenericTypeVariable2<T>(T obj) {Int32 x = (Int32)(Object)obj; // 不報錯String s = (String)(Object)obj; // 不報錯 }現(xiàn)在雖然能編譯通過,但運行時也無法保證是正確的。
轉型為引用類型時還可以使用c# is或者as操作符。
2) 將一個泛型類型變量設為默認值
將泛型類型變量設為null是非法的,除非將泛型類型約束成引用類型。
private void SettingAGenericTypeVariableToNull<T>() {//T temp = null; // 錯誤, 值類型不能設置為null,可考慮使用default('T') }由于未對T進行約束,所以它可能是值類型,而將值類型的變量設為null是不可能的。如果T被約束成引用類型,將temp設為null就是合法的,代碼能順利編譯并運行。
?????? c#團隊認為有必要允許開發(fā)人員將變量設為它的默認值,并專門為此提供了default關鍵字
private void SettingAGenericTypeVariableToDefaultValue<T>() {T temp = default(T); // 正確 }?????? 以上代碼的default關鍵字告訴c#編譯器和clr的jit編譯器,如果t是引用類型,就將temp設為null;如果是值類型,就將temp的所有位設為0.
3) 將一個泛型類型變量與null進行比較
無論泛型類型是否非約束,使用==或!=操作符將一個泛型類型變量與null進行比較都是合法的。
private void ComparingAGenericTypeVariableWithNull<T>(T obj) {if (obj == null) { /* 對值類型來說,永遠不會執(zhí)行 */ } }?????? 調用這個方法時,如果為類型參數(shù)傳遞值類型,那么jit編譯器知道if語句永遠都不會為true,所以不會為if測試或者大括號內的代碼生成本機代碼。
如果T被約束成一個struct,C#編譯器會報錯。值類型的變量不能與null進行比較,因為結果始終一樣。
4)兩個泛型類型變量相互比較
如果泛型類型參數(shù)不是一個引用類型,對同一個泛型類型的兩個變量進行比較是非法的:
private void ComparingTwoGenericTypeVariables<T>(T o1, T o2) {//if (o1 == o2) { } // 錯誤 }?????? 在這個例子中,t未進行約束。雖然兩個引用類型的變量相互比較是合法的,但兩個值類型的變量相互比較是非法的,除非值類型重載了==操作符。如果t被約束城class,上述代碼能通過編譯。
?????? 寫代碼比較基元類型時,c#編譯器知道如何生成正確的代碼。然而,對非基元值類型,c#編譯器不知道如何生成代碼進行比較。所以,如果ComparingTwoGenericTypeVariables方法被約束成struct,編譯器會報錯。
?????? 不允許將類型參數(shù)約束成具體的值類型,因為值類型隱式密封,不可能存在從值類型派生的類型。如果允許將類型參數(shù)約束成具體的值類型,那么泛型方法會被約束為只支持該具體類型,這還不如不用泛型。
?
5)泛型類型變量作為操作書使用
?????? 最后要注意,將操作符應用于泛型類型的操作數(shù)會出現(xiàn)大量問題。在基元類型的那篇文章中,我們指出c#知道如何解釋應用于基元類型的操作符(比如+,-)。但不能將這些操作符應用于泛型類型的變量。編譯器在編譯時確定不了類型,所以不能向泛型類型的變量引用任何操作符。所以,不可能寫出一個能處理任何數(shù)值數(shù)據(jù)數(shù)據(jù)類型的算法。
private T Sum<T>(T num) where T : struct {T sum = default(T);for (T n = default(T); n < num; n++)sum += n;return sum; }?????? 我千方百計想讓這個方法通過編譯。我將T約束成一個struct,而且將default(T)和sum和n初始化為0。但編譯時得到以下錯誤:
error? cs0019 運算符“<”無法應用于T和T類型的操作數(shù)
error? cs0019 運算符“+=”無法應用于T和T類型的操作數(shù)
?????? 這是clr的泛型支持體系的一個嚴重限制,許多開發(fā)人員(尤其是科學、金融和數(shù)學領域的開發(fā)人員)對這個限制感到很失望。許多人嘗試使用各種技術避開這個限制,其中包括反射、dynamic基元類型和操作符重載。但所有這些技術都會嚴重損害性能或者影響代碼可讀性。
?
轉載于:https://www.cnblogs.com/qixinbo/p/10510261.html
總結
以上是生活随笔為你收集整理的重温CLR(八 ) 泛型的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux 终端控制-- 多彩输出 格式
- 下一篇: poj 3662 Telephone L