C#編程中的泛型

來源:互聯網
上載者:User
.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 {    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;                }            }        }    }public void BubbleSort(byte[] 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;                }            }        }    }}


OK,我們再一次解決了問題,儘管總覺得哪裡有點彆扭,但是這段代碼已經能夠工作,按照敏捷式軟體開發 (Agile Software Development)的思想,不要過早地進行抽象和應對變化,當變化第一次出現時,使用最快的方法解決它,當變化第二次出現時,再進行更好的構架和設計。


這樣做的目的是為了避免過度設計,因為很有可能第二次變化永遠也不會出現,而你卻花費了大量的時間精力製造了一個永遠也用不到的“完美設計”。


這很像一個諺語,“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 };sorter.BubbleSort(array);


相信你已經發覺,其實上面所做的一切實現了一個泛型類。這是泛型的一個最典型的應用,可以看到,通過使用泛型,我們極大地減少了重複代碼,使我們的程式更加清爽,泛型類就類似於一個模板,可以在需要時為這個模板傳入任何我們需要的類型。


我們現在更專業一些,為這一節的預留位置起一個正式的名稱,在.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()方法的實現吧,為了避免你回頭再去翻上一節的代碼,我將它複製了下來:

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;            }        }    }}


儘管我們很不情願,但是問題還是出現了,既然是排序,那麼就免不了要比較大小,大家可以看到在兩個元素進行交換時進行了大小的比較,那麼現在請問: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替換原則,關於這個原則我這裡無法進行專門的講述,只能提一下:這個原則要求方法內部不應該對方法所接受的參數進行向下的強制轉換。


為什麼呢?我們定義繼承體系的目的就是為了代碼通用,讓基類實現通用的職責,而讓子類實現其本身的職責,當你定義了一個接受基類的方法時,設計本身是優良的,但是當你在方法內部進行強制轉換時,就破壞了這個繼承體系,因為儘管方法的簽名是面向介面編程,方法的內部還是面向實現編程。


NOTE:什麼是“向下的強制轉換(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) {        int length = array.Length;        for (int i = 0; i <= length - 2; i++) {            for (int j = length - 1; j >= 1; j--) {                                // 對兩個元素進行交換                if (array[j].CompareTo(array[j - 1]) < 0 ) {                    T temp = array[j];                    array[j] = array[j - 1];                    array[j - 1] = temp;                }            }        }    }}


此時我們再次運行上面定義的代碼,會看到下面的輸出:


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) {        return 0;    }    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()方法時,建立Calculator執行個體時也得接受一個型別參數。


為瞭解決這個問題,我們自然而然地會想到:有沒有辦法把型別參數T加到方法上,而非整個類上,也就是降低T作用的範圍。答案是可以的,這便是本小節的主題:泛型方法。類似地,我們只要修改一下SpeedSort()方法的簽名就可以了,讓它接受一個型別參數,此時SuperCalculator的定義如下:

public class SuperCalculator{    // CODE:其他實現略    public void SpeedSort<T>(T[] array) where T : IComparable {        // CODE:實現略    }}


接下來我們編寫一段代碼來對它進行一個測試:

Book[] bookArray = new Book[2];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 總結


本節中我們學習了掌握泛型所需要的最基本知識,你看到了需要泛型的原因,它可以避免重複代碼,還學習到了如何使用型別參數和泛型方法。擁有了本節的知識,你足以應付日常開發中的大部分情境。

以上就是C#編程中的泛型的內容,更多相關內容請關注topic.alibabacloud.com(www.php.cn)!

  • 相關文章

    聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.