8、泛型,泛型
首先陳述,這是一篇水文,因為文字內容比較干,主要是一些介紹和概念。但對泛型理解還是有一定的幫助。沒有過多的代碼解釋,勿怪。
引入:.net2.0發布之後,C#編程語言開始支持泛型,用以增強它的特性,基於此,在基礎類庫中引入了一個以集合為中心的新命名空間:System.Collections.Generic命名空間。
第一個問題:為什麼要引入泛型。其實也就是和數組之間的區別之處。
1、之前提到過C#的數組,數組這種數據結構可以提供一組固定上限的同一類型項。但有很多時候,我們卻需要更靈活的數據結構,例如,可以增長和收縮的容器。以及容器可以保存符合某個條件的對象。
2、類型安全和性能優勢。(約束)
既然如此,那麼就先從非泛型集合開始看起,逐漸過渡。
一、非泛型集合
我們知道,我們經常用mscorlib.dll中的System.Collections命名空間中很多類來組織數據。例如,ArrayList、Hashtable、Queue、Stack、SortedList。
使用這些類將會導致許多問題。
1、低性能。特別在操作數據結構的時候(值類型)。當我們在經典的集合類中存儲結構時,CLR必須執行大量的內存轉換。
2、有些經典的集合類不是類型安全的,因為他們是為了操作System.Object類而開發的。因此,可以包含任何類型。
強調:任何使用.net2.0或者更高版本創建的項目都應該放棄使用System.Collections中的類,而使用System.Collections.Generic中的類。
第一個問題,性能
之前提到,.net支持兩大數據類型:值類型和引用類型。有時候我們需要使用一個類別的變量表示另一個類別的變量,於是C#提供了成為裝箱的簡單機制。這允許我們將值類型保存在引用類型變量中。
裝箱:顯示的將值類型分配給System.Object變量的過程。這個過程,CLR會在堆上分配新的對象並將值類型的值復制到那個實例上,返回給我們的是新分配在堆上對象的引用。
拆箱:與裝箱相反,把保存在對象引用中的值轉換回棧上的相應值類型。(更像是類型轉換操作,但語義上卻迥然不同,因為拆箱CLR會驗收值類型是不是等價於裝箱的類型,如果是則將值類型復制回本地棧變量上。)所以與轉換不同,拆箱必須回到合適的數據類型。
注意:Console.WriteLine()方法操作的對象也是Object類型,故而當輸出的時候,我們的值會被再次裝箱。
裝箱拆箱發生的步驟如下:
1.必須在托管堆上分配一個新對象。
2。基於棧數據的值必須被轉移到新分配的內存位置。
3.在拆箱時,保存在堆上的對象中的值必須轉移回棧。
4.堆上無用的對象(最後)會被回收。
由此可見,當我們操作大數據量的時候,建會產生大量的裝箱拆箱操作,性能就是一個很大的問題了。理想情況下,我們應該可以在沒有任何性能問題的容器中操作站數據,並且在獲取數據的時候也不必使用try、catch作用域。(這正是泛型所實現的。)
第二個問題,類型安全
前面提到,在System.Collections命名空間下大多數類所操作的都是Object類型,因此他們可以容納任何類型,這就導致了類型安全問題。
而我們大多數情況下,需要一個類型安全的容器來操作特定的數據類型。而為了做到這些,我們需要自定義類型集合。
然而,自定義集合,卻並沒有消除裝箱拆箱的損失,例如不管用哪種類型來保存整數,我們都不可避免的使用非泛型容器帶來的裝箱問題。
這就需要使用泛型,泛型可以消除上述帶來的損耗和安全問題。與非泛型相比,泛型的優勢如下:
1、更好的性能,不會導致裝箱拆箱損耗。
2、類型安全,他們只包含指定類型。
3、大幅度減少了構建自定義類型集合的需要,因為基礎類庫提供了幾個預制的容器。
二、泛型類型參數的作用。
例如:List<T>
尖括號中的標記的正式名稱為類型參數,通俗的可以將其稱為占位符。<T>讀作of T。
在創建泛型對象、實現泛型接口或調用泛型成員時,為泛型參數提供的值是由開發者決定的。
1、當我們為泛型類或結構指定特定的類型參數時候,例如Person類的泛型集合,List<Person>。當創建List<T>變量的時候,編譯器並沒有為List<T>創建全新的實現,而只處理你實際調用的泛型類型的成員。
2、除了可以給泛型類指定類型成員,還可以死給泛型成員指定類型成員(即方法和屬性。)。例如List<int> intList;還有泛型接口,interface IComparable<T>
三、System.Collections.Generic命名空間
之前我們學習過對象初始化語法,現在我們看一下集合初始化語法:
這種語法特性,讓你可以用與填充基礎數組類似的語法,來填充ArrayList或List<Y>等容器。
說明,只能支持Add()方法的類使用集合初始化語法,這是ICollection<T>或ICollection接口決定的。
如建立一個point集合類,可以如下表示:
List<Point> mylit = new List<Point>
{
new point{x=1,y=1},
new point{x=2,y=2},
...
};
這種語法的好處是減少鍵盤輸入,但是壞處則是影響可讀性。
此命名空間下,包括幾種泛型類。
List<T>,使用最廣,用以動態調整的類型。
Stack<T>,棧,後進先出的數據集合,包含push和pop方法,分別比歐式進棧和出棧。
Queue<T>,隊列,先進先出的數據集合,包括Dequeue和Enqueue方法,分別表示在移除開始處對象(並返回)和在隊列末尾添加一個對象,除此之外,還有Peek方法,返回隊列開始處對象,但並不移除。
SortedSet<T>,這個類中的項是自動排序的,在插入和移除之後,也能自動排序,因此該類十分有用。
四、創建自定義的泛型方法/泛型結構和類。
傳統方法,交換兩個整數:
Swap(ref int a, ref int b){ int temp; temp = a; a = b; b = temp;}
如果我們繼續要交換兩個Person對象,則我們必須編寫一個swap的新版本:
Swap(ref Person a, ref Person b){ Person temp; temp = a; a = b; b = temp;}
如果需要交換其他的數據,則需要定義更多的方法,這就會給維護帶來麻煩,如果想要有單一的方法,我們需要創建操作Object類型的方法,但這樣會導致裝箱、拆箱,類型安全,顯示轉換等問題。
如果你要創建的方法重載只是輸入參數不同,可以使用泛型。方法如下:
Swap<T>(ref T a, ref T b){T temp; temp = a; a = b; b = temp;}
我們還可以創建泛型類或結構:public class Point<T>{}
其中,我們可以是一個雙精度的point也可以是一個int的point。
1、default(T),和泛型一塊兒使用的時候,它表示一個類型參數的默認值。這非常有用,因為一個泛型類型預先並不知道實際的真為輔,因此無法安全的假設默認值是什麼。默認值如下:
數值的默認值為0;引用類型的默認值是null;一個結構的字段被設為0(值類型)或null(引用類型)。
2、泛型基類,泛型類可以作為其他類的基類,它可以定義許多虛方法和抽象方法。但是要做到這些,泛型類需要遵循一些守則:
首先,如果一個非泛型類型擴展了一個泛型類,派生類必須指定一個類型參數。public class B:A<string>{}.其中A類為我們自定義的泛型類。
其次,如果泛型基類定義了泛型虛方法或抽象方法,派生類型必須使用指定類型參數重寫泛型方法。
最後,如果派生類型也是泛型,則它能夠(可選的)重用類型占位符。不過要注意派生類必須遵照基類中的任何約束。
五,類型參數的約束
如本節描述,任何泛型都必須至少有一個類型參數,並在與泛型類型或參數交互時指定該類型參數,這可以使我們構建類型安全的代碼。
.net平台使用where關鍵字可以得到更加具體的類型參數信息。下面幾種約束表示如下:
where T:struct,該類型參數<T>必須在其繼承鏈中包含System.ValueType值類型,即必須為結構。
where T : class,和上面相反,不能包含值類型,必須為引用類型。
where T : new(),該類型參數<T>必須包含一個默認的構造函數,因為無法預知子定義構造函數格式,所以如果泛型類型必須創建一個類型參數的實例,這將是非常有用的。注意,在有多個約束的時候,此約束必須列在末尾。
where T : NameOfBaseClass,該類型參數<T>必須派生於NameOfBaseClass指定的類。
where T : NameOfInterface,同上,必須派生於指定接口,多個接口必須用逗號隔開。
例如,如果要指定泛型智能操作結構,可以這樣:
Swap<T>(ref T a, ref T b) where T : struct {....}。以這種方式約束了Swap方法就不能再對其他如string對象進行交換了,因為string是引用類型。
六,小結
本小節主要陳述了泛型對比之前的經典容器有的一些優勢。類型安全、高效、減少代碼量。除此之外,還知道了泛型約束的一些知識。