1.1 C#中的泛型
.Net 1.1版本最受诟病的一個缺陷就是沒有提供對泛型的支援。通過使用泛型,我們可以極大地提高代碼的重用度,同時還可以獲得強類型的支援,避免了隐式的裝箱、拆箱,在一定程度上提升了應用程式的性能。本文将系統地為大家讨論泛型,我們先從了解泛型開始。
1.1 了解泛型
1.1.1 為什麼要有泛型?
我想不論大家通過什麼方式進入了計算機程式設計這個行業,都免不了要面對
資料結構和算法這個話題。因為它是計算機科學的一門基礎學科,往往越是底層的部分,對于資料結構或者算法的時間效率和空間效率的要求就越高。比如說,當你在一個集合類型(例如ArrayList)的執行個體上調用Sort()方法對它進行排序時,.Net架構在底層就應用了
快速排序算法。.Net架構中快速排序方法名稱叫QuickSort(),它位于Array類型中,這可以通過Reflector.exe工具檢視到。
我們現在并不是要讨論這個QuickSort()實作的好不好,效率高還是不高,這偏離了我們的主題。但是我想請大家思考一個問題:如果由你來實作一個排序算法,你會怎麼做?好吧,我們把題目限定得再窄一些,我們來實作一個最簡單的冒泡排序(Bubble Sort)算法,如果你沒有使用泛型的經驗,我猜測你可能會毫不猶豫地寫出下面的代碼來,因為這是大學教程的标準實作:
public class SortHelper{
public void BubbleSort(int[] array) {
int length = array.Length;
for (int i = 0; i <= length - 2; i++) {
for (int j = length - 1; j >= 1; j--) {
// 對兩個元素進行交換
if (array[j] < array[j - 1] ) {
int temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
}
}
}
對冒泡排序不熟悉的讀者,可以放心地忽略上面代碼的方法體,它不會對你了解泛型造成絲毫的障礙,你隻要知道它所實作的功能就可以了:将一個數組的元素按照從小到大的順序重新排列。我們對這個程式進行一個小小的測試:
class Program {
static void Main(string[] args) {
SortHelper sorter = new SortHelper();
int[] array = { 8, 1, 4, 7, 3 };
sorter.BubbleSort(array);
foreach(int i in array){
Console.Write("{0} ", i);
}
Console.WriteLine();
Console.ReadKey();
輸出為:
1 3 4 7 8
我們發現它工作良好,欣喜地認為這便是最好的解決方案了。直到不久之後,我們需要對一個byte類型的數組進行排序,而我們上面的排序算法隻能接受一個int類型的數組,盡管我們知道它們是完全相容的,因為byte類型是int類型的一個子集,但C#是一個強類型的語言,我們無法在一個接受int數組類型的地方傳入一個byte數組。好吧,沒有關系,現在看來唯一的辦法就是将代碼複制一遍,然後将方法的簽名改一個改了:
public class SortHelper {
int length = array.Length;
for (int i = 0; i <= length - 2; i++) {
for (int j = length - 1; j >= 1; j--) {
// 對兩個元素進行交換
if (array[j] < array[j - 1]) {
int temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
public void BubbleSort(byte[] array) {
OK,我們再一次解決了問題,盡管總覺得哪裡有點别扭,但是這段代碼已經能夠工作,按照靈活軟體開發的思想,不要過早地進行抽象和應對變化,當變化第一次出現時,使用最快的方法解決它,當變化第二次出現時,再進行更好的構架和設計。這樣做的目的是為了避免過度設計,因為很有可能第二次變化永遠也不會出現,而你卻花費了大量的時間精力制造了一個永遠也用不到的“完美設計”。這很像一個諺語,“fool me once,shame on you. fool me twice, shame on me.”,翻譯過來的意思是“愚弄我一次,是你壞;愚弄我兩次,是我蠢”。
美好的事情總是很難長久,我們很快需要對一個char類型的數組進行排序,我們當然可以仿照byte類型數組的作法,繼續采用複制粘貼大法,然後修改一下方法的簽名。但是很遺憾,我們不想讓它愚弄我們兩次,因為誰也不想證明自己很蠢,是以現在是時候思考一個更佳的解決方案了。
我們仔細地對比這兩個方法,會發現這兩個方法的實作完全一樣,除了方法的簽名不同以外,沒有任何的差別。如果你曾經開發過Web站點程式,會知道對于一些浏覽量非常大的站點,為了避免伺服器負擔過重,通常會采用靜态頁面生成的方式,因為使用Url重寫仍要要耗費大量的伺服器資源,但是生成為html靜态網頁後,伺服器僅僅是傳回用戶端請求的檔案,能夠極大的減輕伺服器負擔。
在Web上實作過靜态頁面生成時,有一種常用的方法,就是模闆生成法,它的具體作法是:每次生成靜态頁面時,先加載模闆,模闆中含有一些用特殊字元标記的占位符,然後我們從資料庫讀取資料,使用讀出的資料将模闆中的占位符替換掉,最後将模闆按照一定的命名規則在伺服器上儲存成靜态的html檔案。
我們發現這裡的情況是類似的,我來對它進行一個類比:我們将上面的方法體視為一個模闆,将它的方法簽名視為一個占位符,因為它是一個占位符,是以它可以代表任何的類型,這和靜态頁面生成時模闆的占位符可以用來代表來自資料庫中的任何資料道理是一樣的。接下來就是定義占位符了,我們再來審視一下這三個方法的簽名:
public void BubbleSort(int[] array)
public void BubbleSort(byte[] array)
public void BubbleSort(char[] array)
會發現定義占位符的最好方式就是将int[]、byte[]、char[]用占位符替代掉,我們管這個占位符用T[]來表示,其中T可以代表任何類型,這樣就屏蔽了三個方法簽名的差異:
public void BubbleSort(T[] array) {
int length = array.Length;
for (int i = 0; i <= length - 2; i++) {
for (int j = length - 1; j >= 1; j--) {
// 對兩個元素進行交換
if (array[j] < array[j - 1]) {
T temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
現在看起來清爽多了,但是我們又發現了一個問題:當我們定義一個類,而這個類需要引用它本身以外的其他類型時,我們可以定義有參數的構造函數,然後将它需要的參數從構造函數傳進來。但是在上面,我們的參數T本身就是一個類型(類似于int、byte、char,而不是類型的執行個體,比如1和'a')。很顯然我們無法在構造函數中傳遞這個T類型的數組,因為參數都是出現在類型執行個體的位置,而T是類型本身,它的位置不對。比如下面是通常的構造函數:
public SortHelper(類型 類型執行個體名稱);
而我們期望的構造函數函數是:
public SortHelper(類型);
此時就需要使用一種特殊的文法來傳遞這個T占位符,不如我們定義這樣一種文法來傳遞吧:
public class SortHelper<T> {
public void BubbleSort(T[] array){
// 方法實作體
我們在類名稱的後面加了一個尖括号,使用這個尖括号來傳遞我們的占位符,也就是類型參數。接下來,我們來看看如何來使用它,當我們需要為一個int類型的數組排序時:
SortHelper<int> sorter = new SortHelper<int>();
int[] array = { 8, 1, 4, 7, 3 };
sorter.BubbleSort(array);
當我們需要為一個byte類型的數組排序時:
SortHelper<byte> sorter = new SortHelper<byte>();
byte [] array = { 8, 1, 4, 7, 3 };
相信你已經發覺,其實上面所做的一切實作了一個泛型類。這是泛型的一個最典型的應用,可以看到,通過使用泛型,我們極大地減少了重複代碼,使我們的程式更加清爽,泛型類就類似于一個模闆,可以在需要時為這個模闆傳入任何我們需要的類型。
我們現在更專業一些,為這一節的占位符起一個正式的名稱,在.Net中,它叫做
類型參數 (Type Parameter),下面一小節,我們将學習類型參數限制。
1.1.2 類型參數限制
實際上,如果你運作一下上面的代碼就會發現它連編譯都通過不了,為什麼呢?考慮這樣一個問題,假如我們自定義一個類型,它定義了書,名字叫做Book,它含有兩個字段:一個是int類型的Id,是書的辨別符;一個是string類型的Title,代表書的标題。因為我們這裡是一個範例,為了既能說明問題又不偏離主題,是以這個Book類型隻含有這兩個字段:
public class Book {
private int id;
private string title;
public Book() { }
public Book(int id, string title) {
this.id = id;
this.title = title;
public int Id {
get { return id; }
set { id = value; }
public string Title {
get { return title; }
set { title = value; }
現在,我們建立一個Book類型的數組,然後試着使用上一小節定義的泛型類來對它進行排序,我想代碼應該是這樣子的:
Book[] bookArray = new Book[2];
Book book1 = new Book(124, ".Net之美");
Book book2 = new Book(45, "C# 3.0揭秘");
bookArray[0] = book1;
bookArray[1] = book2;
SortHelper<Book> sorter = new SortHelper<Book>();
sorter.BubbleSort(bookArray);
foreach (Book b in bookArray) {
Console.WriteLine("Id:{0}", b.Id);
Console.WriteLine("Title:{0}\n", b.Title);
可能現在你還是沒有看到會有什麼問題,你覺得上一節的代碼很通用,那麼讓我們看得再仔細一點,再看一看SortHelper類的BubbleSort()方法的實作吧,為了避免你回頭再去翻上一節的代碼,我将它複制了下來:
array[j - 1] = temp;
盡管我們很不情願,但是問題還是出現了,既然是排序,那麼就免不了要比較大小,大家可以看到在兩個元素進行交換時進行了大小的比較,那麼現在請問:book1和book2誰比較大?小張可能說book1大,因為它的Id是124,而book2的Id是45;而小王可能說book2大,因為它的Title是以“C”開頭的,而book1的Title是以“.”開頭的(字元排序時“.”在“C”的前面)。但是程式就無法判斷了,它根本不知道要按照小張的标準進行比較還是按照小王的标準比較。這時候我們就需要定義一個規則進行比較。
在.Net中,實作比較的基本方法是實作IComparable接口,它有泛型版本和非泛型兩個版本,因為我們現在正在講解泛型,而可能你還沒有領悟泛型,為了避免你的思維發生“死鎖”,是以我們采用它的非泛型版本。它的定義如下:
public interface IComparable {
int CompareTo(object obj);
假如我們的Book類型已經實作了這個接口,那麼當向下面這樣調用時:
book1.CompareTo(book2);
如果book1比book2小,傳回一個小于0的整數;如果book1與book2相等,傳回0;如果book1比book2大,傳回一個大于0的整數。
接下來就讓我們的Book類來實作IComparable接口,此時我們又面對排序标準的問題,說通俗點,就是用小張的标準還是小王的标準,這裡就讓我們采用小張的标準,以Id為标準對Book進行排序,修改Book類,讓它實作IComparable接口:
public class Book :IComparable {
// CODE:上面的實作略
public int CompareTo(object obj) {
Book book2 = (Book)obj;
return this.Id.CompareTo(book2.Id);
為了節約篇幅,我省略了Book類上面的實作。還要注意的是我們并沒有在CompareTo()方法中去比較目前的Book執行個體的Id與傳遞進來的Book執行個體的Id,而是将對它們的比較委托給了int類型,因為int類型也實作了IComparable接口。順便一提,大家有沒有發現上面的代碼存在一個問題?因為這個CompareTo ()方法是一個很“通用”的方法,為了保證所有的類型都能使用這個接口,是以它的參數接受了一個Object類型的參數。是以,為了獲得Book類型,我們需要在方法中進行一個向下的強制轉換。如果你熟悉面向對象程式設計,那麼你應該想到這裡違反了Liskov替換原則,關于這個原則我這裡無法進行專門的講述,隻能提一下:這個原則要求方法内部不應該對方法所接受的參數進行向下的強制轉換。為什麼呢?我們定義繼承體系的目的就是為了代碼通用,讓基類實作通用的職責,而讓子類實作其本身的職責,當你定義了一個接受基類的方法時,設計本身是優良的,但是當你在方法内部進行強制轉換時,就破壞了這個繼承體系,因為盡管方法的簽名是面向接口程式設計,方法的内部還是面向實作程式設計。
注釋:什麼是“向下的強制轉換(downcast)”?因為Object是所有類型的基類,Book類繼承自Object類,在這個金字塔狀的繼承體系中,Object位于上層,Book位于下層,是以叫“向下的強制轉換”。
好了,我們現在回到正題,既然我們現在已經讓Book類實作了IComparable接口,那麼我們的泛型類應該可以工作了吧?不行的,因為我們要記得:泛型類是一個模闆類,它對于在執行時傳遞的類型參數是一無所知的,也不會做任何猜測,我們知道Book類現在實作了IComparable,對它進行比較很容易,但是我們的SortHelper<T>泛型類并不知道,怎麼辦呢?我們需要告訴SortHelper<T>類(準确說是告訴編譯器),它所接受的T類型參數必須能夠進行比較,換言之,就是實作IComparable接口,這便是本小節的主題:泛型限制。
為了要求類型參數T必須實作IComparable接口,我們像下面這樣重新定義SortHelper<T>:
public class SortHelper<T> where T:IComparable {
// CODE:實作略
上面的定義說明了類型參數T必須實作IComaprable接口,否則将無法通過編譯,進而保證了方法體可以正确地運作。因為現在T已經實作了IComparable,而數組array中的成員是T的執行個體,是以當你在array[i]後面點選小數點“.”時,VS200智能提示将會給出IComparable的成員,也就是CompareTo()方法。我們修改BubbleSort()類,讓它使用CompareTo()方法來進行比較:
public class SortHelper<T> where T:IComparable
{
public void BubbleSort(T[] array) {
if (array[j].CompareTo(array[j - 1]) < 0 ) {
T temp = array[j];
此時我們再次運作上面定義的代碼,會看到下面的輸出:
Id:45
Title:.Net之美
Id:124
Title:C# 3.0揭秘
除了可以限制類型參數T實作某個接口以外,還可以限制T是一個結構、T是一個類、T擁有構造函數、T繼承自某個基類等,但我覺得将這些每一種用法都向你羅列一遍無異于浪費你的時間。是以我不在這裡繼續讨論了,它們的概念是完全一樣的,隻是聲明的文法有些差異罷了,而這點差異,相信你可以很輕松地通過檢視MSDN解決。
1.1.3 泛型方法
我們再來考慮這樣一個問題:假如我們有一個很複雜的類,它執行多種基于某一領域的科學運算,我們管這個類叫做SuperCalculator,它的定義如下:
public class SuperCalculator {
public int SuperAdd(int x, int y) {
return 0;
public int SuperMinus(int x, int y) {
public string SuperSearch(string key) {
return null;
public void SuperSort(int[] array) {
由于這個類對算法的要求非常高,.Net架構内置的快速排序算法不能滿足要求,是以我們考慮自己實作一個自己的排序算法,注意到SuperSearch()和SuperSort()方法接受的參數類型不同,是以我們最好定義一個泛型來解決,我們将這個算法叫做SpeedSort(),既然這個算法如此之高效,我們不如把它定義為public的,以便其他類型可以使用,那麼按照前面兩節學習的知識,代碼可能類似于下面這樣:
public class SuperCalculator<T> where T:IComparable {
// CODE:略
public void SpeedSort(T[] array) {
// CODE:實作略
這裡穿插講述一個關于類型設計的問題:确切的說,将SpeedSort()方法放在SuperCaculator中是不合适的。為什麼呢?因為它們的職責混淆了,SuperCaculator的意思是“超級電腦”,那麼它所包含的公開方法都應該是與計算相關的,而SpeedSort()出現在這裡顯得不倫不類,當我們發現一個方法的名稱與類的名稱關系不大時,就應該考慮将這個方法抽象出去,把它放置到一個新的類中,哪怕這個類隻有它一個方法。
這裡隻是一個示範,我們知道存在這個問題就可以了。好了,我們回到正題,盡管現在SuperCalculator類确實可以完成我們需要的工作,但是它的使用卻變得複雜了,為什麼呢?因為SpeedSort()方法污染了它,僅僅為了能夠使用SpeedSort()這一個方法,我們卻不得不将類型參數T加到SuperCalculator類上,使得即使不調用SpeedSort()方法時,建立SuperCalculator執行個體時也得接受一個類型參數。
為了解決這個問題,我們自然而然地會想到:有沒有辦法把類型參數T加到方法上,而非整個類上,也就是降低T作用的範圍。答案是可以的,這便是本小節的主題:泛型方法。類似地,我們隻要修改一下SpeedSort()方法的簽名就可以了,讓它接受一個類型參數,此時SuperCalculator的定義如下:
public class SuperCalculator{
// CODE:其他實作略
public void SpeedSort<T>(T[] array) where T : IComparable {
接下來我們編寫一段代碼來對它進行一個測試:
Book book1 = new Book(124, "C# 3.0揭秘");
Book book2 = new Book(45, ".Net之美");
SuperCalculator calculator = new SuperCalculator();
calculator.SpeedSort<Book>(bookArray);
因為SpeedSort()方法并沒有實作,是以這段代碼沒有任何輸出,如果你想看到輸出,可以簡單地把上面冒泡排序的代碼貼進去,這裡我就不再示範了。這裡我想說的是一個有趣的編譯器能力,它可以推斷出你傳遞的數組類型以及它是否滿足了泛型限制,是以,上面的SpeedSort()方法也可以像下面這樣調用:
calculator.SpeedSort(bookArray);
這樣盡管它是一個泛型方法,但是在使用上與普通方法已經沒有了任何差別。
1.1.4 總結
本節中我們學習了掌握泛型所需要的最基本知識,你看到了需要泛型的原因,它可以避免重複代碼,還學習到了如何使用類型參數限制和泛型方法。擁有了本節的知識,你足以應付日常開發中的大部分場景。
在下面兩節,我們将繼續泛型的學習,其中包括泛型在集合類中的應用,以及泛型的進階話題。