日常生活中的事物都是有型別的,比如我們說“一個女人”,那麼“女”就是這個人的型別。我們可以說“女人都是水做的”,那麼聽者都知道這是在說“女”這種型別的人。再比如你去肉店買肉,你可以對老闆說“我要十斤豬肉”,那麼老闆一定知道你是在要“豬”這種型別的肉。
日常生活中的這些語言都是帶有型別的,但是在日常生活中還有一些語言是不帶型別的。比如我們經常說“人是貪婪的”,這裡的人就沒有型別之分,聽者都知道是指所有的人;我們也可以在肉店裡指著豬肉說“給我來十斤肉”,肉店老闆同樣知道你要的是豬肉。
程式語言必須能夠對現實中的資料進行表述,對於C#語言來講可以使用資料型別對資料進行精確的描述。事實上這種程式語言被稱作強型別語言,在這樣的語言當中出現的資料都必須帶有資料,這樣的語言還有很多,比如C++、Java、Python等。與強型別語言對應的得是弱型別語言,比如VB、JavaScript等,他們沒有資料型別概念。從肉店買肉這個例子我們可以看出這兩種型別的各自的優缺點。
強型別語言顯然可以精確的表達邏輯但表達過於羅嗦,無論是肉店老闆還是旁邊的人聽到“我要十斤豬肉”這句話都可以精確的知道你的意思。弱型別語言的特點就是表達簡潔但邏輯容易發生混亂,比如你還可以指著豬肉說“來十斤”,很顯然你的話只有肉店老闆先看懂你的手勢才能懂,容易引起邏輯的混亂。
計算機程式是推理性語言,中間某一行邏輯出錯都會導致最終的結果出現錯誤,所以從這個角度出發,顯然在買豬肉這個問題上強型別語言獲勝。我們再來看關於人的那個表述,對於“人是貪婪的”這句話,是在描述一種通用性的規律。
對於這個問題用傳統的強型別語言來描述就是“女人是貪婪的,男人是貪婪的”,這樣說顯然非常囉嗦,這也是強型別語言都存在一個缺陷。比如在程式中經常會用到某些通用的演算法,用強型別語言編寫這些通用的演算法會和上面出現一樣的情況,需要每種資料型別都提供一個相同的演算法。泛型技術就是用可以用來解決此類問題。
重點:
Ø 理解泛型的概念
Ø 泛型的定義及其應用
Ø 泛型類
預習功課:
Ø 泛型的概念
Ø 如何定義泛型及其應用
Ø 如何使用泛型類
9.1 為什麼使用泛型
假如讓你用C#編寫一個求兩個數和的方法,你會怎麼做?若求的兩個數是整數,可以定義如下方法:
1 2 3 |
int Add(int a,int b) { return a+b; } |
若求的是兩個double型的數的和,可以定義如下方法:
1 2 3 |
static double Add(double a,double b) { return a+b; } |
若是字串型的數值進行相加,那麼你就可以定義如下方法:
1 2 3 4 5 6 7 |
static double Add(string a,string b) { return double.Parse(a)+double.Parse(b); } |
假如有一天程式需要升級,你需要其他資料型別求和的演算法,日不char、long、decimal等,那你怎麼辦?繼續過載嗎?還是想一個更好更通用的方法?我們可能會想到使用object類,於是你寫了下面這個通用的演算法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
staticobject Add(object a,object b) { //decimal為最大的數值型別,所以使用它 return decimal.Parse(a)+decimal.Parse(b); } static voidMain(string[]args) { decimal r1=(decimal)Add(3,3); decimal r2=(decimal)Add(3.3,3.3); decimal r3=(decimal)Add("3.3","3.3"); Console.WriteLine("{0},{1},{2}",r1,r2,r3) ; } staticobject Add(object a,object b) { returnConvert.ToDecimal(a)+Convert.ToDecimal(b); } |
執行結果:
6,6.6,6.6
這裡用到的技術就是裝箱和拆箱,Add方法首先將所有資料型別的資料進行裝箱,這樣就統一了它們的型別,然後再進行型別轉換和計算,計算結果再拆箱就是要求的結果。實際上就是“泛型”思想的一個應用,這裡用一個通用方法解決了幾乎任何數值型別兩個數的求和操作。所以可以說,對於這個求和演算法來講是通用的、泛型的(不需要特定資料型別)。
但是我們從上面的內碼表可以看到問題,就是它執行了頻繁的裝箱和拆箱操作,我們知道這些操作是非常損耗效能的。另外,裝箱和拆箱的程式碼也顯得比較“難看”
因為每次都要進行強型別轉換,有沒有更好的方式讓我們編寫這種通用演算法呢?於是,C#從2.0版本開始引入了泛型技術,泛型能夠給我們帶來的兩個明顯好處是—程式碼清晰和減少了裝箱、拆箱。
9.2 C#泛型簡介
利用泛型解決交換兩數的泛型方法的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
using System; class Program { static void Main(string[]args) { int i=1,j=2; Console.WriteLine("交換前:{0},{1}",i,j); Swap(ref I,ref j); //交換兩個數 Console.WriteLine("交換後:{0}",i,j); } //交換兩個數的泛型演算法 static void Swap(ref T a,ref T b) { T temp=a ; a=b ; b=temp ; } } |
執行結果:
交換前:1,2
交換後:2,1
這個交換演算法不僅支援任何數字型別,它還支援你在程式中能用到得任何型別。注意,泛型不屬於任何名稱空間,準確的講,泛型是一種編譯技術。在書寫演算法的時候,泛型技術允許我們使用一種型別佔位符(或稱之為型別引數,這裡使用的佔位符是“T”)作為型別的識別符號,而不需要指定特定型別。
當我們在呼叫這個演算法的時候,編譯器使用指定的型別代替型別佔位符建立一個針對這種型別的演算法。這就是泛型技術,它允許你編寫演算法的時候不指定具體型別,但呼叫的時候一定要指定具體型別,編寫演算法的時候使用“<>”來指定型別佔位符,呼叫的時候一般也使用“<>”來指定具體的資料型別。
上面這個例子中的Swap,指定了這個泛型方法的佔位符是“T”,指定後我們就可以認為有了這麼一個資料型別,該型別就是T型別,然後這個T型別既可以作為引數的資料型別又可以作為方法的返回值型別,還可以在方法內部作為區域性變數的資料型別。當我們通過Swap(ref i,ref j)來呼叫這個泛型方法時,在編譯時Swap方法中所有出現“T”的地方都會被“int”型別所代替,也就相當於我們建立了
int型的交換方法,如:
1 2 3 4 5 6 7 8 9 10 11 |
static void Swap(ref int a,ref int b) { int temp=a; a=b; b=temp; } |
l 程式碼重用
泛型最突出優點就是可以程式碼重用。從上面舉的交換演算法的例子你也可以看出節省了多少程式碼。對於一個程式設計師來講,寫的好的演算法是很重要的財富,例如我們一直在使用各種類庫,這些類庫實際上就是一些優秀的程式設計師封裝的,我們直接呼叫就是一個程式碼重用的過程。
l 型別安全
型別安全的含義是型別之間的操作必須是相容的,反之就是型別不安全。型別不安全的程式碼會在執行時出現異常,比如兩個數相加的演算法,Convert.ToDecimal(a),a是object型別,a可以是數值“3.3”,a也可以是普通字串“hello”,如果a是後者那麼執行型別轉換時必定會出異常,所以說使用Convert.ToDecimal(a)是型別不安全的做法,同樣那個求和的方法也是型別不安全的方法。泛型本質上還是強型別的,如果你使用一個不相容的型別來呼叫泛型演算法,編譯器是會報錯的,所以說泛型是型別安全的。
l 效能更佳
相比裝箱和拆箱,泛型效率更高一些。因裝箱時系統需要分配記憶體,而拆箱時需要型別轉換,這兩個操作都是極其耗費效能的。特別是在執行一些大資料量的演算法時(比如排序、搜尋等)裝箱和拆箱效能損耗尤其嚴重,因此,在C#中提倡使用泛型。
9.3 泛型定義及其應用
使用泛型可以定義泛型方法、泛型類、泛型介面等。在這些泛型結構的定義中,泛型型別引數(或叫佔位符)是必須指定的,型別引數所包含的型別就是我們定義的泛型型別,我們可以一次性定義多個泛型型別,如泛型方法Swap<T,U,Z>三個泛型型別。型別引數一般放在所定義的類、方法、介面等識別符號後面,並且包含在“<>”裡面。
泛型型別名稱的寫法也有一定的規則:
l 泛型型別名稱必須是由字母、數字、下劃線組成,並且必須以字元或下劃線開頭。比如_T、T、TC都是有效的泛型型別名稱。
l 務必使用有意義泛型型別名稱,除非單個字母名稱完全可以讓人瞭解它表示的含義,如T.
l 當型別引數裡只有單個泛型型別時,考慮使用T作為泛型型別名,如class Note。
l 提倡作為泛型型別名的字首,如Tkey,TValue。
前面舉例子的時候,一般使用了泛型型別T,但從本質上講我們可以使用滿足上面要求的任何單詞。實際上,泛型型別名和類名或介面名的定義規則基本一樣。
9.4 泛型結構體
結構是值型別,通常可以定義結構型別來表示一些簡單的物件。比如,我們前面接觸的系統結構體Point、DateTime等。但是,這些結構體通常都儲存一種型別的資料,我們可以定義一個泛型結構體,它將可以儲存任何資料,定義規則:
1 2 3 4 5 6 7 |
struct 結構名 <泛型型別列表> { 結構體; } |
要注意泛型型別識別符號的定義只能放在結構名的後面,下面我們定義了一個
Point型別的泛型結構體,此時該結構體的X、Y可以儲存任何數值型別的座標資料。程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
classProgram { //定義泛型結構體和泛型型別T struct Point { public T X; public T Y; } //測試泛型結構體 static voidMain(string[]args) { //給T型別指定資料型別為int型 Point a =newPoint(); X=1; a. Y=2; Console.WriteLine("{0},{1}",a.X,a.Y); } } |
執行結果:
1,2
9.5 泛型類
泛型類封裝不屬於特定具體資料型別的資料或操作。泛型類最常見的就是泛型集合類,如連結串列、雜湊表、堆疊、佇列、樹等。對於集合的操作,如從集合中新增、移除、排序等操作大體上都以相同方式進行的,與所儲存資料型別無關,即可使用泛型技術。
在泛型類中使用的資料型別,可以是泛型型別也可以是普通的。一般規則是,類中使用的泛型型別越多,程式碼就會變得越靈活,重用性就越好。但是要注意,類中如果有太多的泛型型別也會使其他開發人員難以閱讀或理解該類。要定義類的泛型型別也是在類名後面通過"<>"定義,類的其他元素除了方法外都不能定義自己的泛型型別,但可以使用該類定義的泛型型別。泛型類定義規則如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
class 類名<泛型型別列表> { //類體 } //示例程式碼: usingSystem; classProgram { //定義泛型類和泛型型別T private class Node { private T data; public Node(T t) { data=t; } public T Data { get{return data;} set{data=value;} } } static void Main() { Nodenode=newNode(10000); Console.WriteLine("資料:{0}",node.Data); Nodesnode=newNode("壹萬"); Console.WriteLine("資料:{0}",snode.Data); } } |
執行結果:
資料:10000
資料:壹萬
如前所述,類中的成員有很多,如欄位、屬性、方法、事件、索引器等,其中除了方法之外,其他的類成員都不能自定義的泛型型別,只能使用定義類的時候定義的泛型型別或系統資料型別:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
classStudent<T,U> { private T name; //姓名 private U[]score; //各個科目的分數陣列 private int ucode; //編號使用系統資料型別 public U this[int n] //返回一個分數 { get{return score[n]; } } |
類中的方法可以是泛型的,泛型方法的定義規則如下:
訪問修飾符 返回型別 方法名<泛型型別列表>(方法引數列表)
如:
1 |
public voidShow(T a){} |
此泛型方法的使用時要給T指定一個實際的資料型別,如:
1 |
Show("hello"); |
其中方法的泛型型別列表中定義的泛型型別可以出現在方法的任何位置,包括返回值、引數、方法內,當然也可以不出現,比如下面這些都是合法的:
1 2 3 4 5 |
public TGet(T a) {return default(T) ;} public intGet public TGet(int a) {return default(T);} |
這上面用了default關鍵字,這個關鍵字可以取當前型別的預設初始值,這個關鍵字對於引用型別會返回null,對於數值型別會返回零。
另外,類中也可以出現泛型的過載方法,如:
1 2 3 4 5 |
voidDoWork(){} voidDoWork(){} voidDoWork<T,U>(){} |
由於方法是在類中,所以泛型方法中的資料型別又三種情況,一種是類的泛型型別,一種是泛型方法自身的泛型型別,另外還可以是系統資料型別。泛型方法和非泛型方法或屬性、索引器可以互相呼叫。如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
classStudent { private U id; private string name; public void ShowHello() { this.Show("hello"); //呼叫泛型方法 } public void ShowId() { this.Show(id); } private void Show(S msg) { Console.WriteLine(msg); } } |
類的泛型型別只能用於本類,方法的泛型型別只能用於本方法。不管誰定義的泛型,一旦定義了泛型型別,你可以就當泛型型別是一個真實的型別來用了。
9.6 典型的泛型類
.Net框架類庫中,System.Collections.Generic和System.Collections.ObjectModel名稱空間中,分別定義了大量的泛型類和泛型介面,這些泛型類多為集合類,因為泛型最大的應用正體現於再集合中對於不同型別物件的管理。
下表列出了,.Net框架中常用的泛型類和泛型介面:
泛型類 | 說明 |
List | 對應於ArrayList集合類,可以動態調整集合容量,通過索引方式訪問物件,支援排序、搜尋和其他常見操作。 |
SortedList<TKey,TValue> | 對應於SortedList集合類,表示Key/Value對集合,類似於SortedDictionary<TKey,TValue>集合類,而SortedList在記憶體上更有優勢。 |
Queue | 對應於Queue集合類,是一種先進先出的集合類,常應用於順序儲存處理。 |
Stack | 對應於Stack集合類,是一種後進先出的集合類。 |
Collection | 對應於CollectionBase集合類,是用於自定義泛型集合的基類,提供了受保護的方法來實現定製泛型集合的行為Collection的例項是可修改的。 |
Dictionary<TKey,TValue> | 對應於Hashtable集合類,表示Key/Value對的集合類,Key必須是唯一的,其元素型別既不是Key的型別,也不是Value的型別,而是KeyValuePair型別。 |