[.net 物件導向程式設計基礎] (18) 泛型
上一節我們說到了兩種資料型別陣列和集合,陣列是指包含同一型別的多個元素,集合是指.net中提供資料儲存和檢索的專用類。
陣列使用前需要先指定大小,並且檢索不方便。集合檢索和宣告方便,但是存在型別安全問題,本來使一個型別安全的C#變得不安全了。
集合為了解決陣列預設大小的問題,採取了一種自動擴容的辦法,這樣當大小不夠時,他就建立一個新的儲存區域,把原有集合的元素複製過來。如此又對效能上也是有很大的影響。
上節我們說到解決這些缺陷的方法,那就是.NET 2.0以後,微軟程式猿們推出來的新特性——泛型。
1.什麼是泛型?
泛型是具有佔位符(型別引數)的類、結構、介面和方法,這些佔位符是類、結構、介面和方法所儲存或使用的一個或多個佔位符。
這個概念聽起來比較繞,其實理解起來也不難,我的理解是類、介面、委託、結構或方法中有型別引數就是泛型型別,這樣就有型別引數的概念。泛型集合類可以將型別引數用作它儲存物件的點位符;型別引數作為其欄位或方法的引數型別出現(這是MSDN中的描述)。
泛型集合所在的命我空間為:System.Collections.Generic
而List類是ArrayList的泛型等效類。該類使用大小按需動態增加的陣列實現IList介面。使用方法就是IList<T>和List<T>,這個T就是你要指定的集合的資料或物件型別。
2.泛型宣告
泛型類: class Name<t>{}
泛型方法: void Name(T t){}
泛型介面:interface IName<T>{}
泛型結構:struct Name<T>{}
泛型委託:public delegate void Name<T>(T param);
3.泛型方法
泛型我們在定義的時候,說明了他是可以使用佔位符來佔位類、結構、介面和方法的。我們先看一下方法使用泛型的例子。
我們還是使用前面的例子來看一下使用泛型:
類之間的關係UML圖如下:
我們呼叫假如要實現,讓每個動物都叫幾聲。該如何寫呢?
1 /// <summary> 2 /// 動物類(父類 抽象類) 3 /// </summary> 4 abstract class Animal 5 { 6 /// <summary> 7 /// 名字 8 /// 說明:類和子類可訪問 9 /// </summary> 10 protected string name; 11 12 /// <summary> 13 /// 建構函式 14 /// </summary> 15 /// <param name="name"></param> 16 public Animal(string name) 17 { 18 this.name = name; 19 } 20 21 private int shoutNum = 8; 22 public int ShoutNum 23 { 24 get { return shoutNum; } 25 set { shoutNum = value; } 26 } 27 28 /// <summary> 29 /// 名字(虛屬性) 30 /// </summary> 31 public virtual string MyName 32 { 33 get { return this.name; } 34 } 35 36 /// <summary> 37 /// 叫聲,這個方法去掉虛方法,把迴圈寫在這裡 38 /// </summary> 39 public void Shout() 40 { 41 string result = ""; 42 for (int i = 0; i < ShoutNum; i++) 43 result += getShoutSound() + "!"; 44 45 Console.WriteLine(MyName); 46 Console.WriteLine(result); 47 } 48 49 /// <summary> 50 /// 建立一個叫聲的虛方法,子類重寫 51 /// </summary> 52 /// <returns></returns> 53 public virtual string getShoutSound() 54 { 55 return ""; 56 } 57 58 /// <summary> 59 /// 讓所有動集合類的動物叫三次並報名字 (泛型) 60 /// </summary> 61 /// <param name="animal"></param> 62 public static void AnimalShout(IList<Animal> animal) 63 { 64 DateTime dt = System.DateTime.Now; 65 foreach (Animal anm in animal) 66 { 67 anm.Shout(); 68 } 69 Console.WriteLine("使用泛型讓所有動物叫一遍所用時間為:" + (System.DateTime.Now - dt).TotalMilliseconds +"毫秒"); 70 } 71 /// <summary> 72 /// 讓所有動集合類的動物叫三次並報名字 (過載方法 集合) 73 /// </summary> 74 /// <param name="animal"></param> 75 public static void AnimalShout(ArrayList animal) 76 { 77 DateTime dt = System.DateTime.Now; 78 foreach (Animal anm in animal) 79 { 80 anm.Shout(); 81 } 82 Console.WriteLine("使用集合讓所有動物叫一遍所用時間為:" + (System.DateTime.Now - dt).TotalMilliseconds + "毫秒"); 83 } 84 85 /// <summary> 86 /// 讓所有動集合類的動物叫三次並報名字 (過載方法 陣列) 87 /// </summary> 88 /// <param name="animal"></param> 89 public static void AnimalShout(Animal[] animal) 90 { 91 DateTime dt = System.DateTime.Now; 92 foreach (Animal anm in animal) 93 { 94 anm.Shout(); 95 } 96 Console.WriteLine("使用陣列讓所有動物叫一遍所用時間為:" + (System.DateTime.Now - dt).TotalMilliseconds + "毫秒"); 97 } 98 } 99 100 101 /// <summary> 102 /// 狗(子類) 103 /// </summary> 104 class Dog : Animal 105 { 106 string myName; 107 public Dog(string name) 108 : base(name) 109 { 110 myName = name; 111 } 112 113 /// <summary> 114 /// 名字(重寫父類屬性) 115 /// </summary> 116 public override string MyName 117 { 118 get { return "我是:狗狗,我叫:" + this.name; } 119 } 120 121 /// <summary> 122 /// 叫(重寫父類方法) 123 /// </summary> 124 public override string getShoutSound() 125 { 126 return "汪!"; 127 } 128 } 129 130 /// <summary> 131 /// 狗(子類) 132 /// </summary> 133 class ShepherdDog : Dog 134 { 135 string myName; 136 public ShepherdDog(string name) 137 : base(name) 138 { 139 myName = name; 140 } 141 142 /// <summary> 143 /// 名字(重寫父類屬性) 144 /// </summary> 145 public override string MyName 146 { 147 get { return "我是:牧羊犬,我叫:" + this.name; } 148 } 149 150 /// <summary> 151 /// 叫(重寫父類方法) 152 /// </summary> 153 public override string getShoutSound() 154 { 155 return "汪~嗚!"; 156 } 157 } 158 159 /// <summary> 160 /// 貓(子類) 161 /// </summary> 162 class Cat : Animal 163 { 164 string myName; 165 public Cat(string name) 166 : base(name) 167 { 168 myName = name; 169 } 170 /// <summary> 171 /// 名字(重寫父類屬性) 172 /// </summary> 173 public override string MyName 174 { 175 get { return "我是:貓咪,我叫:" + this.name; } 176 177 } 178 179 /// <summary> 180 /// 叫(重寫父類方法) 181 /// </summary> 182 public override string getShoutSound() 183 { 184 return "喵!"; 185 } 186 } 187 188 /// <summary> 189 /// 貓(子類) 190 /// </summary> 191 class PersianCat : Cat 192 { 193 string myName; 194 public PersianCat(string name) 195 : base(name) 196 { 197 myName = name; 198 } 199 /// <summary> 200 /// 名字(重寫父類屬性) 201 /// </summary> 202 public override string MyName 203 { 204 get { return "我是:波斯貓,我叫:" + this.name; } 205 206 } 207 208 /// <summary> 209 /// 叫(重寫父類方法) 210 /// </summary> 211 public override string getShoutSound() 212 { 213 return "喵~嗚!"; 214 } 215 } 216 217 /// <summary> 218 /// 羊(子類) 219 /// </summary> 220 class Sheep : Animal 221 { 222 string myName; 223 public Sheep(string name) 224 : base(name) 225 { 226 myName = name; 227 } 228 /// <summary> 229 /// 名字(重寫父類屬性) 230 /// </summary> 231 public override string MyName 232 { 233 get { return "我是:羊羊,我叫:" + this.name; } 234 235 } 236 /// <summary> 237 /// 叫(重寫父類方法) 238 /// </summary> 239 public override string getShoutSound() 240 { 241 return "咩!"; 242 } 243 }
呼叫方法:
1 //陣列 2 Animal[] animalArray = new Animal[] { new Dog("旺財"), new Cat("小花"), new Cat("阿狸"), new Sheep("純羊"), new Dog("小白"), new ShepherdDog("汪羊"), new PersianCat("機貓") }; 3 4 //泛型 5 IList<Animal> animal = new List<Animal>(); 6 animal.Add(new Dog("旺財")); 7 animal.Add(new Cat("小花")); 8 animal.Add(new Cat("阿狸")); 9 animal.Add(new Sheep("純羊")); 10 animal.Add(new Dog("小白")); 11 animal.Add(new ShepherdDog("汪羊")); 12 animal.Add(new PersianCat("機貓")); 13 14 //集合 15 ArrayList animalArrayList = new ArrayList(); 16 animalArrayList.Add(new Dog("旺財")); 17 animalArrayList.Add(new Cat("小花")); 18 animalArrayList.Add(new Cat("阿狸")); 19 animalArrayList.Add(new Sheep("純羊")); 20 animalArrayList.Add(new Dog("小白")); 21 animalArrayList.Add(new ShepherdDog("汪羊")); 22 animalArrayList.Add(new PersianCat("機貓")); 23 24 25 //呼叫過載方法看它們的執行叫8次並報名字所需時間 26 Animal.AnimalShout(animalArray); 27 Animal.AnimalShout(animal); 28 Animal.AnimalShout(animalArrayList); 29 Console.ReadLine();
執行結果如下:
以上的例項並沒有模擬出能客觀測試效率的環境,因為根據我們的經驗陣列並不能接受不同型別的元素,而集合和泛型可以,如果使用不同資料測試,也不是客觀的。以上例項主要反映了泛型、陣列、集合的使用方法,小夥伴們不要太糾結測試時間,不過泛型的時間確實是比較快的。有了泛型小夥伴們就不要再使用ArrayList這個不安全型別了。
陣列、List和ArrayList的區別:
上節說了陣列和集合ArrayList的區別,這節我們使用了泛型,再說一下他們三者的區別
陣列:
(1)在記憶體中是連續儲存的,所以它的索引速度是非常的快,而且賦值與修改元素也很簡單。
(2)但是陣列也存在一些不足的地方。比如在陣列的兩個資料間插入資料也是很麻煩的,還有我們在宣告陣列的時候,必須同時指明陣列的長度,陣列的長度過長,會造成記憶體浪費,陣列和長度過短,會造成資料溢位的錯誤。這樣如果在宣告陣列時我們並不清楚陣列的長度,就變的很麻煩了.
集合ArrayList:
集合的出現就是為了解決陣列的缺陷,但他本身也有缺陷,直到.NET 2.0以後出現泛型,我們可以說這是微軟設計上的失誤。
(1).ArrayList並非型別安全
ArrayList不論什麼型別都接受,實際是接受一個object型別。
比如如下操作:
ArrayList ar = new ArrayList();
ar.Add(111);
ar.Add("bbb");
我們使用foreach遍歷的時候 foreach(int array in ar){}那麼遇到”bbb”則程度報錯,因此我們說他是非安全型別。
(2).遍歷ArrayList資源消耗大
因此型別的非安全,我們在使用ArrayList的時候,就意味著增加一個元素,就需要值型別轉換為Object物件。遍歷的時候,又需要將Object轉為值型別。
就是裝箱(boxing,指將值型別轉換為引用型別) 和
拆箱(unboxing,指將引用型別轉換為值型別)
由於裝箱了拆箱頻繁進行,需要大量計算,因此開銷很大。
泛型List:
List和AraayList都是繼承於IList,屬於等效類,他們之間的區別也是對集合ArrayList侷限性的修改
(1)型別安全,通過允許指定泛型類或方法操作的特定型別,泛型功能將型別安全的任務從您轉移給了編譯器。不需要編寫程式碼來檢測資料型別是否正確,因為會在編譯時強制使用正確的資料型別。減少了型別強制轉換的需要和執行時錯誤的可能性。
(2)減少開銷,泛型提供了型別安全但沒有增加多個實現的開銷。
3.泛型類
對於泛型類,我們先解決一下實際例子:假如我們有一個泛型類,不知道是什麼型別,在初始化的時候再指定型別。類裡面有一個方法,可以接受初始化的引數型別,也就是一個泛型方法。
/// <summary> /// 泛型有一個泛型方法,該方法有一個泛型引數 /// </summary> /// <typeparam name="T"></typeparam> class MyClass<T> { public static T F(T param) { return param; } }
呼叫:
//泛型類呼叫 int param = 2, param2= 3; string result1 = (MyClass<string>.F(param.ToString()) + param2).ToString(); string result2 = (MyClass<int>.F(param) + param2).ToString(); Console.WriteLine(result1); Console.WriteLine(result2); Console.ReadLine();
可以看到,輸出結果為: 23 和 5 ,使用泛型,我們不會因為資料型別不同,就需要去複製一個方法去處理了。
4.型別約束
我們上面定義了泛型,他們預設沒有型別約束,就是說可以是任意型別。既然定義了泛型,為何還要約束,其實我們這麼理解,泛型首先是一種安全型別,約束他只是讓他的範圍更小一點而已。
比如某些情況下,我們需要約束型別
主要有以下六種型別的約束:
約束 |
說明 |
T:結構 |
型別引數必須是值型別。可以指定除 Nullable 以外的任何值型別。有關更多資訊,請參見使用可空型別(C# 程式設計指南)。 |
T:類 |
型別引數必須是引用型別,包括任何類、介面、委託或陣列型別。 |
T:new() |
型別引數必須具有無引數的公共建構函式。當與其他約束一起使用時,new() 約束必須最後指定。 |
T:<基類名> |
型別引數必須是指定的基類或派生自指定的基類。 |
T:<介面名稱> |
型別引數必須是指定的介面或實現指定的介面。可以指定多個介面約束。約束介面也可以是泛型的。 |
T:U |
為 T 提供的型別引數必須是為 U 提供的引數或派生自為 U 提供的引數。這稱為裸型別約束。 |
型別約束的特點:
A.指定的就是型別引數必須是引用型別,
B.包括任何類、介面、委託或陣列型別,如果除了這幾樣就是非法的了
C.可以輸入任何的引用型別同時也確定了範圍,防止了輸入值型別引發的不安全
D.約束的型別必須是非封閉的型別
E.建議不要對型別引數使用 == 和 != 運算子,因為這些運算子僅測試引用同一性而不測試值相等性
F.如果有new(),則一定要放在最後
未繫結的型別引數
沒有約束的型別引數(如公共類 SampleClass<T>{} 中的 T)稱為未繫結的型別引數。未繫結的型別引數具有以下規則:
·不能使用 != 和 == 運算子,因為無法保證具體型別引數能支援這些運算子。
·可以在它們與 System.Object 之間來回轉換,或將它們顯式轉換為任何介面型別。
·可以將它們與 null 進行比較。將未繫結的引數與 null 進行比較時,如果型別引數為值型別,則該比較將始終返回 false。
裸型別約束
·用作約束的泛型型別引數稱為裸型別約束。
·當具有自己的型別引數的成員函式需要將該引數約束為包含型別的型別引數時,裸型別約束很有用,泛型類的裸型別約束的作用非常有限,因為編譯器除了假設某個裸型別約束派生自 System.Object 以外,不會做其他任何假設。
·在希望強制兩個型別引數之間的繼承關係的情況下,可對泛型類使用裸型別約束。
5.約束舉例說明
下面對這幾種約束舉例說明
還是以這個動物系列為例,下面是實現程式碼:
1 /// <summary> 2 /// 動物類(父類 抽象類) 3 /// </summary> 4 abstract class Animal 5 { 6 /// <summary> 7 /// 名字 8 /// 說明:類和子類可訪問 9 /// </summary> 10 protected string name; 11 12 /// <summary> 13 /// 建構函式 14 /// </summary> 15 /// <param name="name"></param> 16 public Animal(string name) 17 { 18 this.name = name; 19 } 20 21 private int shoutNum = 3; 22 public int ShoutNum 23 { 24 get { return shoutNum; } 25 set { shoutNum = value; } 26 } 27 28 /// <summary> 29 /// 名字(虛屬性) 30 /// </summary> 31 public virtual string MyName 32 { 33 get { return this.name; } 34 } 35 36 /// <summary> 37 /// 叫聲,這個方法去掉虛方法,把迴圈寫在這裡 38 /// </summary> 39 public void Shout() 40 { 41 string result = ""; 42 for (int i = 0; i < ShoutNum; i++) 43 result += getShoutSound() + "!"; 44 45 Console.WriteLine(MyName); 46 Console.WriteLine(result); 47 } 48 49 /// <summary> 50 /// 建立一個叫聲的虛方法,子類重寫 51 /// </summary> 52 /// <returns></returns> 53 public virtual string getShoutSound() 54 { 55 return ""; 56 } 57 } 58 59 /// <summary> 60 /// 宣告一個介面 ISpeak(講話) 61 /// </summary> 62 interface ISpeak 63 { 64 void Speak(); 65 } 66 67 /// <summary> 68 /// 狗(子類) 69 /// </summary> 70 class Dog : Animal 71 { 72 string myName; 73 public Dog(string name): base(name) 74 { 75 myName = name; 76 } 77 /// <summary> 78 /// 名字(重寫父類屬性) 79 /// </summary> 80 public override string MyName 81 { 82 get { return "我是:狗狗,我叫:" + this.name; } 83 } 84 /// <summary> 85 /// 叫(重寫父類方法) 86 /// </summary> 87 public override string getShoutSound() 88 { 89 return "汪!"; 90 } 91 } 92 93 /// <summary> 94 /// 貓(子類) 95 /// </summary> 96 class Cat : Animal 97 { 98 string myName; 99 public Cat(string name): base(name) 100 { 101 myName = name; 102 } 103 /// <summary> 104 /// 名字(重寫父類屬性) 105 /// </summary> 106 public override string MyName 107 { 108 get { return "我是:貓咪,我叫:" + this.name; } 109 } 110 /// <summary> 111 /// 叫(重寫父類方法) 112 /// </summary> 113 public override string getShoutSound() 114 { 115 return "喵!"; 116 } 117 } 118 119 /// <summary> 120 /// 藍貓(子類) 121 /// 繼承 Cat和介面ISpeak 122 /// </summary> 123 class BlueCat : Cat,ISpeak 124 { 125 string myName; 126 public BlueCat(string name) : base(name) 127 { 128 myName = name; 129 } 130 /// <summary> 131 /// 名字(重寫父類屬性) 132 /// </summary> 133 public override string MyName 134 { 135 get { return "我是:藍貓,我叫:" + this.name; } 136 } 137 138 /// <summary> 139 /// 實現介面ISpeak的成員Speak 140 /// </summary> 141 public void Speak() 142 { 143 Console.WriteLine("我會說人話:“你好,我叫:" + this.name + "~~~”"); 144 } 145 } 146 147 /// <summary> 148 /// 羊(子類) 149 /// </summary> 150 class Sheep : Animal 151 { 152 string myName; 153 public Sheep(string name): base(name) 154 { 155 myName = name; 156 } 157 /// <summary> 158 /// 名字(重寫父類屬性) 159 /// </summary> 160 public override string MyName 161 { 162 get { return "我是:羊羊,我叫:" + this.name; } 163 } 164 /// <summary> 165 /// 叫(重寫父類方法) 166 /// </summary> 167 public override string getShoutSound() 168 { 169 return "咩!"; 170 } 171 } 172 173 /// <summary> 174 /// 喜羊羊(子類) 175 /// 繼承 Sheep和介面ISpeak 176 /// </summary> 177 class PleasantSheep : Sheep, ISpeak 178 { 179 string myName; 180 public PleasantSheep(string name) : base(name) 181 { 182 myName = name; 183 } 184 /// <summary> 185 /// 名字(重寫父類屬性) 186 /// </summary> 187 public override string MyName 188 { 189 get { return "我是:喜羊羊,我叫:" + this.name; } 190 } 191 /// <summary> 192 /// 實現介面ISpeak的成員Speak 193 /// </summary> 194 public void Speak() 195 { 196 Console.WriteLine("我會說人話:“你好,我叫:" + this.name + "~~~”"); 197 } 198 }
5.1基類約束
這個比較常見,就是約束型別的實參都必須繼承同一基類
我們新建一個泛型類CatShout,約束它的基類實參為Cat類
//泛型約束 - 基類約束 class CatShout<T> where T : Cat { public void Shout(T cat) { cat.Shout(); } }
呼叫及結果:
//泛型約束- 基類約束 CatShout<Cat> cat = new CatShout<Cat>(); cat.Shout(new BlueCat("蘭喵")); Console.ReadLine(); //CatShout<Dog> dog = new CatShout<Dog>(); 假如使用 Dog類例項化,約束生效,程式報錯
5.2 介面約束
這次我們建立一個泛型類AnimalSpeak類,約束它派生自介面ISpeak
//泛型約束 - 介面約束 class AnimalSpeak<T> where T : ISpeak { public void Speak(T t) { t.Speak(); } }
呼叫及結果:
//泛型約束- 介面約束 AnimalSpeak<BlueCat> animalSpeak = new AnimalSpeak<BlueCat>(); animalSpeak.Speak(new BlueCat("蘭喵")); AnimalSpeak<PleasantSheep> animalSpeak2 = new AnimalSpeak<PleasantSheep>(); animalSpeak2.Speak(new PleasantSheep("肥咩")); //假如使用以下呼叫,約束生效,程式報錯,因為Cat並不繼承介面ISpeak,不符合介面約束 //AnimalSpeak<Cat> animalSpeak3 = new AnimalSpeak<Cat>(); //animalSpeak2.Speak(new Cat("阿狸"));
說明:可以同時約束為類和介面,類在前面,介面在後。介面可以有多個,類只能一個,不可以繼承多個類。
6.0 要點:
最後總結一下
A.泛型基本的使用比較簡單,簡單說就是一個取代陣列和集合的安全型別
B.泛型包括,泛型類、方法、介面、委託、結構。
C.泛型在某些情況下為了呼叫更加安全,即在編譯階段就進行校驗,經常使用約束
D.泛型的型別約束有幾個方面:類約束,介面,建構函式,結構等。基類約束、介面約束和建構函式約束較為常見
E.類的約束使用where關鍵詞,約束的幾個方面有先後順序,通常類,介面,建構函式,這樣的順序。
如果感覺約束這塊兒有點難理解,小夥伴們先掌握好基本泛型使用方法。
==============================================================================================
返回目錄
<如果對你有幫助,記得點一下推薦哦,有不明白的地方或寫的不對的地方,請多交流>
==============================================================================================