[.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關鍵詞,約束的幾個方面有先后順序,通常類,接口,構造函數,這樣的順序。
如果感覺約束這塊兒有點難理解,小伙伴們先掌握好基本泛型使用方法。
==============================================================================================
返回目錄
<如果對你有幫助,記得點一下推薦哦,有不明白的地方或寫的不對的地方,請多交流>
==============================================================================================