泛型
泛型相比反射,委托等較為抽象的概念要更接地氣得多,而且在平常工作時,我們幾乎時刻都和泛型有接觸。大部分人對泛型都是比較熟悉的。
泛型集合是類型安全的集合。相對于泛型System.Collections.Generic,我們有類型不安全的集合System.Collections,其中的成員均為Object類型。一個經典的例子是ArrayList。
在使用ArrayList時,我們可以插入任意類型的資料,如果插入值類型的資料,其都會裝箱為Object類型。這造成類型不安全,我們不知道取出的資料是不是想要的類型。泛型(集合)的資料類型是統一的,是類型安全的,沒有裝箱和拆箱問題,提供了更好的性能。為泛型變量設定預設值時常使用default關鍵字進行:T temp = default(T)。如果T為引用類型,則temp為null,如果T為值類型,則temp為0。
ArrayList的泛型集合版本為List<T>。T稱為類型參數。調用時指定的具體類型叫做實際參數(實參)。
面試必須知道的泛型三大好處:類型安全,增強性能,代碼複用。
泛型集合的使用契機:幾乎任何時候,都不考慮不用泛型集合代替泛型集合。很多非泛型集合也有了自己的泛型版本,例如棧,隊列等。
泛型方法的使用契機一般為傳入類型可能有很多種,但處理方式卻相同的情境。這時我們可以不需要寫很多個重載,而考慮用泛型方法達到代碼複用的目的。配合泛型限制,可以寫出更嚴謹的方法。泛型委托也可以看成是泛型方法的一種應用。
例如交換兩個同類型變量的值:


限制的作用是限制能指定成泛型實參(即T的具體類型)的數量。通過限制類型的數量,可以對這些類型執行更多的操作。例如下面的方法,T被限制為必須是實作了IComparable接口的類型。此時,傳入的T除了擁有object類型的方法之外,還額外多了一個CompareTo方法。由于保證了傳入的T必須是實作了IComparable接口的類型,就可以肯定T類型一定含有CompareTo方法。如果去掉限制,o1是沒有CompareTo方法的。
此時如果将object類型的資料傳入方法,則會報錯。因為object沒有實作IComparable<T>接口。
泛型限制分為如下幾類:
接口限制:泛型實參必須實作某個接口。接口限制可以有多個。
基類型限制:泛型實參必須是某個基類的派生類。特别的,可以指定T : class / T : struct,此時T分别隻能為引用類型或值類型。基類型限制必須放在其他限制之前。
構造函數new()限制:泛型實參必須具有可通路的無參數構造函數(預設的也可)。new()限制出現在where子句的最後。
如果泛型方法沒有任何限制,則傳入的對象會被視為object。它們的功能比較有限。不能使用 != 和 == 運算符,因為無法保證具體類型參數能支援這些運算符。
可變性是以一種類型安全的方式,将一個對象作為另一個對象來使用。其對應的術語則是不變性(invariant)。
可變性是以一種類型安全的方式,将一個對象作為另一個對象來使用。例如對普通繼承中的可變性:若某方法聲明傳回類型為Stream,在實作時可以傳回一個MemoryStream。可變性有兩種類型:協變和逆變。
協變性:可以建立一個較為一般類型的變量,然後為其指派,值是一個較為特殊類型的變量。例如:
因為string肯定是一個object,是以這樣的變化非常正常。
逆變性:在上面的例子中,我們無法将str和一個新的object對象畫等号。如果強行要實作的話,隻能這麼幹:
但這樣還是會在運作時出錯。這也告訴我們,逆變性是很不正常的。
協變性和out關鍵字搭配使用,用于向調用者傳回某項操作的值。例如下面的接口僅有一個方法,就是生産一個T類型的執行個體。那麼我們可以傳入一個特定類型。如我們可以将IFactory<Pizza>視為IFactory<Food>。這也适用于Food的所有子類型。(即将其視為一個更一般類型的實作)
逆變性則相反,和in關鍵字搭配使用,指的是API将會消費值,而不是生産值。此時一般類型出現在參數中:
這意味着如果我們實作了IPrint<Code>,我們就可以将其當做IPrint<CsharpCode>使用。(即将其視為一個更具體類型的實作)
如果存在雙向的傳遞,則什麼也不會發生。這種類型是不變體(invariant)。
這個接口是不變體。我們不能将它視為一個更具體或更一般類型的實作。
假設有如下繼承關系People –> Teacher,People –> Student。
如果我們以協變的方式使用(假設你建立了一個IStorage< Teacher >的執行個體,并将其視為IStorage<People>)則我們可能會在調用Serialize時産生異常,因為Serialize方法不支援協變(如果參數是People的其他子類,例如Student,則IStorage< Teacher >将無法序列化Student)。
如果我們以逆變的方式使用(假設你建立了一個IStorage<People>的執行個體,并将其視為IStorage< Teacher >),則我們可能會在調用Deserialize時産生異常,因為Deserialize方法不支援逆變,它隻能傳回People不能傳回Teacher。
如果類型參數用于輸出,就使用out,如果用于輸入,就使用in。注意,協變和逆變性展現在泛型類T和T的派生類。目前out 和in 關鍵字隻能在接口和委托中使用。
IEnumerable<T>支援協變性,它允許一個類似下面簽名
的方法,該方法傳入更具體的類型(T的派生類),但在方法内部,類型會被看成IEnumerable<T>。注意out關鍵字。
下面的例子示範了協變性。我們利用IEnumerable<T>的協變性,傳入較為具體的類型Circle。編譯器會将其看成較為抽象的類型Shape。


IComparer支援逆變性。我們可以簡單的實作一個可以比較任何圖形面積的方法,傳入的輸入類型(in)是最General的類型IShape。之後,在使用時,我們獲得的結果是較為具體的類型Circle。因為任何圖形都可以比較面積,圓形當然也可以。
注意IComparer的簽名是public interface IComparer<in T>。


1. 不支援類的類型參數的可變性。隻有接口和委托可以擁有可變的類型參數。in 和 out 修飾符隻能用來修飾泛型接口和泛型委托。
2. 可變性隻支援引用轉換。可變性隻能用于引用類型,禁止任何值類型和使用者定義的轉換,如下面的轉換是無效的:
将 IEnumerable<int> 轉換為 IEnumerable<object> ——裝箱轉換
将 IEnumerable<short> 轉換為 IEnumerable<int> ——值類型轉換
将 IEnumerable<string> 轉換為 IEnumerable<XName> ——使用者定義的轉換
3. 類型參數使用了 out 或者 ref 将禁止可變性。對于泛型類型參數來說,如果要将該類型的實參傳給使用 out 或者 ref 關鍵字的方法,便不允許可變性,如:
這段代碼編譯器會報錯。
4. 可變性必須顯式指定。從實作上來說編譯器完全可以自己判斷哪些泛型參數能夠逆變和協變,但實際卻沒有這麼做,這是因為C#的開發團隊認為:必須由開發者明确的指定可變性,因為這會促使開發者考慮他們的行為将會帶來什麼後果,進而思考他們的設計是否合理。
5. 多點傳播委托與可變性不能混用。下面的代碼能夠通過編譯,但是在運作時會抛出 ArgumentException 異常:
這是因為負責連結多個委托的 Delegate.Combine方法要求參數必須為相同的類型,而上面的兩個泛型委托的輸出一個為字元串,另一個為object。上面的示例我們可以修改成如下正确的代碼:
此時兩個泛型委托的輸出均為object。
以下的代碼中,接口IBar中有一個方法,其接受另一個接口IFoo作為參數。IFoo是支援協變的。這樣會出現一個問題。


假設T為字元串類型。則如果有一類Bar <T>: IBar<T>,另一類Foo<T>:IFoo<T>,則Bar的某個執行個體應該可以這樣調用方法:aBar.Test (foo)。


當調用方法之後,傳入的參數類型是Foo<object>。我們再看看方法的簽名:
現在我們的aBar的類型參數T是string,是以,我們期待的Test方法的傳入類型也應該是IFoo<string>,或者能夠變化成IFoo<string>的類型,但傳入的卻是一個object。是以,這兩個接口的方法的寫法是有問題的。
當把IFoo接口的簽名改用out修飾之後,問題就解決了。此時由于允許逆變,Foo<object>就可以變化成IFoo<string>了。不過本人眼光短淺,目前還沒發現這個特點在實際工作中有什麼應用。
http://www.cnblogs.com/LoveJenny/archive/2012/03/13/2392747.html
http://www.cnblogs.com/xinchufa/p/3524452.html
http://www.cnblogs.com/Ninputer/archive/2008/11/22/generic_covariant.html