泛型是CLR和編程語言提供的一種特殊機制,它用於滿足“演算法重用” 。 可以想象一下一個只有操作的參數的數據類型不同的策略模式,完全可以用泛型來化為一個函數。 以下是它的優勢: 這就是為什麼List<T>淘汰了ArrayList的原因,特別是在進行值類型操作時,因為裝箱拆箱過多而差距很大。 約定:泛型
泛型是CLR和編程語言提供的一種特殊機制,它用於滿足“演算法重用” 。
可以想象一下一個只有操作的參數的數據類型不同的策略模式,完全可以用泛型來化為一個函數。
以下是它的優勢:
- 類型安全
- 給泛型演算法應用一個具體的數據類型時,如果不相容這種類型,就會編譯錯誤或者報異常。
- 更清晰的代碼
- 減少了強制轉換,讓代碼更簡潔
- 更佳的性能
- 用泛型可以有效避免裝箱拆箱的操作,且無需在進行強制轉換時驗證是否類型安全,這兩點都有效提高了代碼的性能。
這就是為什麼List<T>淘汰了ArrayList的原因,特別是在進行值類型操作時,因為裝箱拆箱過多而差距很大。
約定:泛型參數要麼為T要麼以大寫T開頭,例如List<T>。
FCL中的泛型
System.Collections.Generic和System.Collections.ObjectModel命名空間中提供了多個泛型集合類和介面。
System.Collections.Concurrent命名空間則提供線程安全的泛型集合類。
System.Array類則提供了大量的靜態泛型方法。
泛型的基礎結構
.net 2.0才有泛型。
- 開放類型和封閉類型
- 之前我們講到CLR會為各種類型創建類型對象,同樣一個新的泛型類TroyList<T>也會創建一個類型對象,我們將具有泛型參數的類型稱為開放類型。
- 不能構造開放類型的實例
- 而指定了泛型類型實參的泛型類型稱為封閉類型,例如:TroyList<int>。
- 可以構造封閉類型的實例
- 如果TroyList<T>定義了靜態欄位或者方法,那麼TroyList<int>和TroyList<string>之間並不共用,因為這其實是兩個不同的類型對象。
- 之前我們講到CLR會為各種類型創建類型對象,同樣一個新的泛型類TroyList<T>也會創建一個類型對象,我們將具有泛型參數的類型稱為開放類型。
- 泛型類型的繼承
- 使用泛型類型並指定類型實參後,實際上是一個新的封閉類型,新的類型對象從泛型類型派生自的那個類型派生。即List<T>派生自Object,那麼List<int>就派生自Object。
- 關於代碼爆炸的優化
- 看到這裡你可能想到了,一個開放類型實際上會有多個封閉類型,比如一個List<T>會有List<int>,List<string>等N多封閉類型。實際上就是N多的類型對象,生成N多的重覆代碼,於是這被稱作代碼爆炸。
- 關於優化:
- 兩個不同的程式集用到同一種封閉類型,只會由JIT編譯器變異一次
- CLR認為所有用引用類型做類型實參的封閉類型完全相同,所以代碼可以共用。也就是說List<String>和List<Stream>的方法編譯後的代碼可以通用。因為操作的不同的引用類型的地址大小都是一樣的。
委托和介面的逆變和協變泛型類型實參
泛型委托和介面的每個泛型類型參數都可標記為協變數和逆變數,利用此功能可實現相同類型但實參類型不同的委托和介面的相互轉換。(很繞,不明白可以看下麵)
- 不變數
- 意味著泛型類型參數不可更改
- 逆變數
- 意味著泛型類型參數可以從一個類更改為它的派生類。用in標記,逆變數泛型類型參數只能出現在輸入位置。
- 協變數
- 意味著泛型類型參數可以從一個類更改為它的基類。用out標記,協變數泛型類型參數只能出現在輸出位置。
舉個例子
public class 基類 { } public class 派生類 : 基類 { } public class Test{ public delegate TResult MyFunc<in T1, out TResult, T2>(T1 a, T2 b);//第一個為逆變數,第二個為協變數,第三個為不變數 void show() { MyFunc<基類, 基類, 基類> fn1 = null; //以下註釋為我自己的理解方式,只是為了方便理解而已 MyFunc<派生類, 基類, 基類> fn2 = fn1;//MyFunc<派生類, 派生類, 基類> fn2 = fn1;轉換錯誤 MyFunc<基類, Object, 基類> fn3 = fn1;//MyFunc<Object, Object, 基類> fn3 = fn1;轉換錯誤 MyFunc<派生類, Object, 基類> fn4 = fn1; } }
依然很繞,實際上不懂也沒關係,轉換不了編譯器自然會提示。瞭解有這個東西就行了,也建議用int和out指定泛型委托的類型變數。更多的時候我們會用自帶的泛型委托Action和Func,這兩個泛型委托的參數都用到in和out。
關於泛型方法的類型推斷
void Go() { String s1 = "213"; Object s2 = "123"; Show(s1, s2);//不指定Show<T>的T的玩法就叫類型推斷,類型推斷通過傳入的變數s1和變數s2的變數類型來推斷,而不是實際類型。因為這裡兩個變數類型不同,所以函數編譯不通過。 } void Show<T>(T a,T b) { }
約束
泛型的約束是一個很有意思的事情。
void Show<T>(T a,T b) where T :IList { }
比如上面這個函數,約束傳入的類型T必須實現了IList介面。
通過約束可以限制傳入的類型,然而正式因為提供了這層約束,保證了傳入的類型都實現了IList介面,我們就可以使用IList的各種方法了。
約束分類:
- 主要約束
- 主要約束可以是代表非密封類的一個引用類型。(可以指定0到1個主要約束)
- 兩個特殊的主要約束為class和struct,分別約束傳入的參數為引用類型和值類型。(特例的特例,struct不能約束Nullable<T>)
- 約束不能指定以下特殊引用類型:Object,Array,Delegate,MulticastDelegate,ValueType,Enum或者Void。
- 次要約束
- 次要約束代表介面類型。(可以指定0到多個次要約束)
- 特殊的次要約束,即指定的兩個泛型類型參數中,一個繼承另一個,例如:where T2:T1。
- 構造器約束
- 構造器約束約束類型實參,一定是實現了公共無參構造函數的非抽象類型。(可以指定0到1個構造器約束)
- 所有值類型都隱式提供了公共無參構造器。所以同時使用struct和new()約束被認為是多餘的,會報錯。
可驗證性
以下幾種情況因為代碼不可驗證是否合法,所以將報錯:
- 泛型類型變數的轉換
- 原因:不可將泛型類型T的變數轉換為其它類型,因為T可能為任何變數,所以可能轉換失敗
-
void Show<T>(T obj){ string a=(string)obj; //出錯 }
- 解決方案:
void Show<T>(T obj) { string a = obj as string;//對於string而言,其實這裡用ToString方法可能更恰當一點 }
值類型可以先強制轉換為object,再轉為具體的值類型。然而我認為這樣的代碼還是需要開箱裝箱的,也許可以考慮修改下演算法。
- 將泛型類型變數設為預設值
- 原因:因為T可以是值類型和引用類型,所以不可能設置一個值類型或者引用類型的預設值
- 解決方案:可以考慮加約束或者用default(T),作為預設值。
- 兩個泛型類型變數相互比較
- 原因:因為非基元類型的值類型除非重載了==操作符,否則會報錯。
- 解決方案:可以考慮約束為class或者用Equals。(註意哦,有可能因為Equals的被覆蓋所以具體不確定是判斷同一性還是相等性)
- 泛型類型變數作為操作數使用
- 原因:因為非基元類型的值類型除非重載了操作符,否則會報錯。
- 解決方案:反射,操作符重載或者dynamic。(會有性能影響,我一般用dynamic了)