C#中的泛型

經過使用泛型,咱們能夠極大地提升代碼的重用度,同時還能夠得到強類型的支持,避免了隱式的裝箱、拆箱,在必定程度上提高了應用程序的性能。html

1.1 理解泛型

1.1.1 爲何要有泛型?

我想不論你們經過什麼方式進入了計算機程序設計這個行業,都免不了要面對數據結構和算法這個話題。由於它是計算機科學的一門基礎學科,每每越是底層的部分,對於數據結構或者算法的時間效率和空間效率的要求就越高。好比說,當你在一個集合類型(例如ArrayList)的實例上調用Sort()方法對它進行排序時,.Net框架在底層就應用了快速排序算法。.Net框架中快速排序方法名稱叫QuickSort(),它位於Array類型中,這能夠經過Reflector.exe工具查看到。web

咱們如今並非要討論這個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,咱們再一次解決了問題,儘管總以爲哪裏有點彆扭,可是這段代碼已經可以工做,按照敏捷軟件開發的思想,不要過早地進行抽象和應對變化,當變化第一次出現時,使用最快的方法解決它,當變化第二次出現時,再進行更好的構架和設計。這樣作的目的是爲了不過分設計,由於頗有可能第二次變化永遠也不會出現,而你卻花費了大量的時間精力製造了一個永遠也用不到的「完美設計」。這很像一個諺語,「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替換原則,關於這個原則我這裏沒法進行專門的講述,只能提一下:這個原則要求方法內部不該該對方法所接受的參數進行向下的強制轉換。爲何呢?咱們定義繼承體系的目的就是爲了代碼通用,讓基類實現通用的職責,而讓子類實現其自己的職責,當你定義了一個接受基類的方法時,設計自己是優良的,可是當你在方法內部進行強制轉換時,就破壞了這個繼承體系,由於儘管方法的簽名是面向接口編程,方法的內部仍是面向實現編程。

註釋:什麼是「向下的強制轉換(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()方法時,建立SuperCalculator實例時也得接受一個類型參數。

爲了解決這個問題,咱們天然而然地會想到:有沒有辦法把類型參數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 總結

本節中咱們學習了掌握泛型所須要的最基本知識,你看到了須要泛型的緣由,它能夠避免重複代碼,還學習到了如何使用類型參數約束和泛型方法。擁有了本節的知識,你足以應付平常開發中的大部分場景。

本文出處:http://www.cnblogs.com/JimmyZhang/archive/2008/12/17/1356727.html轉載只爲學習。謝謝分享!

相關文章
相關標籤/搜索