往往一些剛接觸C#程式設計的初學者,對於泛型的認識就是直接跳到對泛型集合的使用上,雖然微軟為我們提供了很多內建的泛型型別,但是如果我們只是片面的瞭解呼叫方式,這會導致我們對泛型盲目的使用。至於為什麼要使用泛型,什麼情況下定義屬於自己的泛型,定義泛型又能為程式帶來哪些好處。要理清這些問題,我們就必須深刻理解泛型的本質,形成泛型程式設計的思維方式。
接下來我將基於一個基礎範例,然後通過需求不斷的演化範例,從而讓泛型在關鍵時刻脫穎而出,以便讓我們能夠深刻體會泛型的作用。假設.NET沒有為我們提供用於儲存資料的集合,而我們需要一個能夠用於儲存string元素的集合,基於這個情況我們自定義了一個用於儲存字串的集合類:
class ArraryStr
{
public ArraryStr()
{
_items = new string[100]; //初始化儲存元素的容量,只是為了演示故將容量定義為固定值
}
private string[] _items; //儲存元素的陣列
private int _count; //元素總數
public int Count
{
get { return _count; }
}
public void Add(string item) //新增元素
{
_items[_count] = item;
_count++;
}
public string this[int index] //索引
{
get { return _items[index]; }
set { _items[index] = value; }
}
} // END ArraryStr
為了驗證自定義string集合的可行性,我們對其進行了如下的應用:
1 ArraryStr arraryStr = new ArraryStr();
2 arraryStr.Add("張三");
3 Console.WriteLine(arraryStr[0]);
目前對於建立string型別的集合已經大功告成,而此刻我們又接到了一個新的需求,即我們需要一個集合儲存int型別的元素。基於自定義string集合的經驗來看,我們可以發現,string集合型別和我們即將要建立的int集合型別的結構和內容幾乎是一樣的。這就意味著我們可以使用江湖盛行的「複製大法」,將之前的程式碼複製一遍,然後輕微修改下即可。下面是兩個集合型別程式碼的對比圖。
在早年有款熱門的遊戲叫做「大家來找茬」,該遊戲主要玩法就是在兩個大致相同的圖片中,查詢兩者之間的細微差異之處。我們使用的「複製大法」,促使我們編寫的程式碼形成了可以用於這個遊戲遊玩的場景。「對於上面的兩個程式碼截圖,你能找出圖中不同的地方嗎?」
對於軟體開發者而言,面對的最主要的敵人就是「變化」,假設後面還會出現N個型別的元素需要我們定義集合來儲存,那我們是不是要將相同的程式碼無窮盡的複製下去?DRY(Don't Repeat Yourself,不要重複自己),請記住這是作為一名軟體開發者編碼的原則,「複製大法」很明顯的違背了這個原則。
通過「複製,貼上」的手段可以很明顯的感受到我們在做重複的事情,在重複中我們可以發現:集合儲存的型別在增加,但是集合的結構和新增元素的方法都是相同的邏輯。簡單來說就是,不同型別的處理,其處理邏輯都是類似的。基於這個特點,為了滿足自定義集合能夠應對所有型別的儲存,我們必須使用一個通用型別來作為代表,此時此刻我們腦海中就能浮現出一句話:object是一切型別的基礎類別。這就意味著我們新增的所有型別,都可以隱式的轉換為object型別,從而使得自定義集合可以新增任何型別的元素。讓我們來運用這個object型別來試試:
class ArraryList
{
public ArraryList() { _items = new object[100]; }
private object[] _items;
private int _count;
public int Count
{
get { return _count; }
}
public void Add(object item)
{
_items[_count] = item;
_count++;
}
public object this[int index]
{
get { return _items[index]; }
set { _items[index] = value; }
}
} // END ArraryStr
internal class Program
{
static void Main(string[] args)
{
ArraryList arraryList = new ArraryList();
arraryList.Add("張三");
arraryList.Add(18);
string name = (string)arraryList[0];
int age = (int)arraryList[1];
} // END Main()
}
在上面的程式碼中,我們結合了object是一切型別基礎類別的特點,對集合型別進行改造,併成功的使用該方式的集合新增了不同型別的元素。雖然在使用的角度來看已經完美無缺(可以新增任何型別),但是獲取集合元素進行賦值的時候,還使用了型別強制轉換的手段。這是因為這種方式存在很嚴重的問題,主要包括以下兩個方面:
到目前位置,我們還是沒有能建立一個能夠儲存任何型別的集合,但是我們可以對於上述的範例演變的過程進行一個總結:對於不同型別有相同處理邏輯的情況,如果一味的複製會導致我們出現重複程式碼,如果使用object來作為解決重複的方案,會存在型別安全和效能的問題。至於如何讓徹底解決這些問題,這就要說到了本文講解的主題——泛型。
C#中有兩種不同的機制來編寫跨型別(一個型別代替多個型別)可複用的程式碼:繼承和泛型。繼承的複用性來自於基礎類別,而泛型的複用性是通過帶有「預留位置」的程式碼模板型別實現的。繼承實現複用是站在物件導向的角度思考的,而泛型的複用是站在實現特定功能上思考的。相比於繼承,泛型不用遵循里氏替換原則,並且能夠提高型別的安全性,減少型別轉換帶來的拆箱和裝箱。
怎麼樣理解泛型?泛型本質上相當於一種「程式碼模板」,可以用一套程式碼,為不同型別的同一邏輯使用統一的方式實現。其中「模板」一詞的概念需要進行深刻的體會。例如,公司在招聘時會與用人方簽訂勞動合同,而這個勞動合同的主要內容對於所有人來說幾乎都是一樣的,只是在極個別的地方有所差異,如薪資、姓名等。所以公司不會為某個人(張三或李四)去特意的制定合同,而是會統一制定一份勞動合同作為模板,將其中針對個人存在差異的部分通過「下劃線」進行佔位預留,「下劃線」的值將在簽訂合同時由具體的聘用者根據自身情況填寫。
對於這種模板方式的使用,公司在制定合同時則不用考慮簽訂合同的人具體是誰,因為勞動合同(模板)和使用者是分開的,所以公司只用專注於合同的主要內容即可。而我們在實際的程式設計運用中,使用泛型的目的,其實和公司制定通用的勞動合同模板是一個道理。假設你的公司需要僱傭100名員工時,你不希望為每一個人都制定一個專屬的合同吧?假設你的程式碼中,如果遇到10個型別,它們的操作處理邏輯都一樣時,你不希望為這個10個型別寫10個處理方式吧?
通過上面的介紹和例子,接下來我們將泛型運用到我們的範例中來,程式碼如下:
1 class ArraryList<T>
2 {
3 public ArraryList() { _items = new T[100]; }
4
5 private T[] _items;
6 private int _count;
7 public int Count
8 {
9 get { return _count; }
10 }
11
12 public void Add(T item)
13 {
14 _items[_count] = item;
15 _count++;
16 }
17
18 public T this[int index]
19 {
20 get { return _items[index]; }
21 set { _items[index] = value; }
22 }
23 } // END ArraryStr
24 internal class Program
25 {
26 static void Main(string[] args)
27 {
28 ArraryList<string> arraryStr = new ArraryList<string>();
29 arraryStr.Add("張三");
30 Console.WriteLine(arraryStr[0]);
31
32 ArraryList<int> arraryInt = new ArraryList<int>();
33 arraryInt.Add(18);
34 Console.WriteLine(arraryInt[0]);
35
36 } // END Main()
37
38 }
在上面的程式碼中,我們將集合型別定義為了泛型類,該型別中出現的T屬於泛型中的型別引數(Type Parameter)。泛型為了達到通用處理的目的,所以不能將某個具體型別作為處理的目標型別,故而將要處理的型別用「T」作為一個型別預留位置。
「T」並不是真正的資料型別,它更像是泛型使用的型別藍圖,所以在使用時,泛型型別的消費者必須將一個具體型別作為「型別引數」傳遞到尖括號內,以此構造一個有明確處理型別的泛型範例。所以我們在外部使用泛型時不能以:「ArraryList<T>list =new ArraryList<T>()」、「T t=new T()」這種方式去範例化泛型型別。另外,「T」本身僅僅是型別引數的名稱,它只是代表了型別引數的標識而已,這意味著我們可以使用其他字元來為型別引數命名。
通過型別引數的使用我們可以得知,泛型型別程式碼在靜態階段沒有明確的型別,那麼在程式執行的時候,它又是如何和使用時指定的「型別引數」進行對接的呢?為了搞清楚這個問題,下面我們來了解下泛型執行時的本質。
我們編寫的C#程式在編譯後生成的程式碼,並不是計算機可以直接執行的程式碼,而是會生成CIL(通用中間語言)程式碼幷包含在程式集中,如果想要生成計算機可執行的程式碼,則還需要JIT(即時編譯器)對CIL程式碼進行二次編譯。然而泛型型別確認其具體型別的時機,就在JIT進行二次編譯時,JIT編譯的程式碼如果包含了泛型的內容,那麼它會根據泛型型別的消費者指定的型別引數,將CIL中泛型程式碼中的預留位置T替換為一個具體的型別,從而明確當前執行的泛型程式碼是針對哪個型別來使用的,其中替換的過程是由CLR在執行時進行主導,JIT來實際操作完成的。這個在執行時確認了型別的泛型又被稱之為「封閉型別」,反之在執行時確認之前的泛型稱為「開放型別」。
泛型使用預留位置在執行時替換具體型別的機制,其實和本文中例舉勞動合同模板使用「下劃線」的方式有同樣的思想。在指定勞動合同模板時,對於聘用者的姓名並不能寫一個具體的名字,因為模板的目的是為了通用化,所以對於名字採用了「下劃線」的方式。當公司與某個具體的人簽訂合同的時候,勞動合同模板中的下劃線將由聘用者根據自身情況填寫。回到泛型中其使用思想也是如此,我們使用泛型的目的是為了讓多個型別的處理通用化,所以在定義泛型程式碼的時候並不能指定一個具體型別,故使用型別引數T進行代替,這個型別引數T就相當於勞動合同模板中的「下劃線」,當泛型在實際執行的時候,JIT會根據泛型消費者指定的具體型別與預留位置T進行替換。