[.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 ///2 /// 动物类(父类 抽象类) 3 /// 4 abstract class Animal 5 { 6 ///7 /// 名字 8 /// 说明:类和子类可访问 9 /// 10 protected string name; 11 12 ///13 /// 构造函数 14 /// 15 /// 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 ///29 /// 名字(虚属性) 30 /// 31 public virtual string MyName 32 { 33 get { return this.name; } 34 } 35 36 ///37 /// 叫声,这个方法去掉虚方法,把循环写在这里 38 /// 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 ///50 /// 创建一个叫声的虚方法,子类重写 51 /// 52 ///53 public virtual string getShoutSound() 54 { 55 return ""; 56 } 57 58 /// 59 /// 让所有动集合类的动物叫三次并报名字 (泛型) 60 /// 61 /// 62 public static void AnimalShout(IListanimal) 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 /// 72 /// 让所有动集合类的动物叫三次并报名字 (重载方法 集合) 73 /// 74 /// 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 ///86 /// 让所有动集合类的动物叫三次并报名字 (重载方法 数组) 87 /// 88 /// 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 ///102 /// 狗(子类)103 /// 104 class Dog : Animal105 {106 string myName;107 public Dog(string name)108 : base(name)109 {110 myName = name;111 }112 113 ///114 /// 名字(重写父类属性)115 /// 116 public override string MyName117 {118 get { return "我是:狗狗,我叫:" + this.name; }119 }120 121 ///122 /// 叫(重写父类方法)123 /// 124 public override string getShoutSound()125 {126 return "汪!";127 }128 }129 130 ///131 /// 狗(子类)132 /// 133 class ShepherdDog : Dog134 {135 string myName;136 public ShepherdDog(string name)137 : base(name)138 {139 myName = name;140 }141 142 ///143 /// 名字(重写父类属性)144 /// 145 public override string MyName146 {147 get { return "我是:牧羊犬,我叫:" + this.name; }148 }149 150 ///151 /// 叫(重写父类方法)152 /// 153 public override string getShoutSound()154 {155 return "汪~呜!";156 }157 }158 159 ///160 /// 猫(子类)161 /// 162 class Cat : Animal163 {164 string myName;165 public Cat(string name)166 : base(name)167 {168 myName = name;169 }170 ///171 /// 名字(重写父类属性)172 /// 173 public override string MyName174 {175 get { return "我是:猫咪,我叫:" + this.name; }176 177 }178 179 ///180 /// 叫(重写父类方法)181 /// 182 public override string getShoutSound()183 {184 return "喵!";185 }186 }187 188 ///189 /// 猫(子类)190 /// 191 class PersianCat : Cat192 {193 string myName;194 public PersianCat(string name)195 : base(name)196 {197 myName = name;198 }199 ///200 /// 名字(重写父类属性)201 /// 202 public override string MyName203 {204 get { return "我是:波斯猫,我叫:" + this.name; }205 206 }207 208 ///209 /// 叫(重写父类方法)210 /// 211 public override string getShoutSound()212 {213 return "喵~呜!";214 }215 }216 217 ///218 /// 羊(子类)219 /// 220 class Sheep : Animal221 {222 string myName;223 public Sheep(string name)224 : base(name)225 {226 myName = name;227 }228 ///229 /// 名字(重写父类属性)230 /// 231 public override string MyName232 {233 get { return "我是:羊羊,我叫:" + this.name; }234 235 }236 ///237 /// 叫(重写父类方法)238 /// 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 IListanimal = new List (); 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.泛型类
对于泛型类,我们先解决一下实际例子:假如我们有一个泛型类,不知道是什么类型,在初始化的时候再指定类型。类里面有一个方法,可以接受初始化的参数类型,也就是一个泛型方法。
////// 泛型有一个泛型方法,该方法有一个泛型参数/// ///class MyClass { public static T F(T param) { return param; }}
调用:
//泛型类调用int param = 2, param2= 3;string result1 = (MyClass.F(param.ToString()) + param2).ToString();string result2 = (MyClass .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 ///2 /// 动物类(父类 抽象类) 3 /// 4 abstract class Animal 5 { 6 ///7 /// 名字 8 /// 说明:类和子类可访问 9 /// 10 protected string name; 11 12 ///13 /// 构造函数 14 /// 15 /// 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 ///29 /// 名字(虚属性) 30 /// 31 public virtual string MyName 32 { 33 get { return this.name; } 34 } 35 36 ///37 /// 叫声,这个方法去掉虚方法,把循环写在这里 38 /// 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 ///50 /// 创建一个叫声的虚方法,子类重写 51 /// 52 ///53 public virtual string getShoutSound() 54 { 55 return ""; 56 } 57 } 58 59 /// 60 /// 声明一个接口 ISpeak(讲话) 61 /// 62 interface ISpeak 63 { 64 void Speak(); 65 } 66 67 ///68 /// 狗(子类) 69 /// 70 class Dog : Animal 71 { 72 string myName; 73 public Dog(string name): base(name) 74 { 75 myName = name; 76 } 77 ///78 /// 名字(重写父类属性) 79 /// 80 public override string MyName 81 { 82 get { return "我是:狗狗,我叫:" + this.name; } 83 } 84 ///85 /// 叫(重写父类方法) 86 /// 87 public override string getShoutSound() 88 { 89 return "汪!"; 90 } 91 } 92 93 ///94 /// 猫(子类) 95 /// 96 class Cat : Animal 97 { 98 string myName; 99 public Cat(string name): base(name)100 {101 myName = name;102 }103 ///104 /// 名字(重写父类属性)105 /// 106 public override string MyName107 {108 get { return "我是:猫咪,我叫:" + this.name; }109 }110 ///111 /// 叫(重写父类方法)112 /// 113 public override string getShoutSound()114 {115 return "喵!";116 }117 }118 119 ///120 /// 蓝猫(子类)121 /// 继承 Cat和接口ISpeak122 /// 123 class BlueCat : Cat,ISpeak124 {125 string myName;126 public BlueCat(string name) : base(name)127 {128 myName = name;129 }130 ///131 /// 名字(重写父类属性)132 /// 133 public override string MyName134 {135 get { return "我是:蓝猫,我叫:" + this.name; }136 }137 138 ///139 /// 实现接口ISpeak的成员Speak140 /// 141 public void Speak()142 {143 Console.WriteLine("我会说人话:“你好,我叫:" + this.name + "~~~”");144 }145 }146 147 ///148 /// 羊(子类)149 /// 150 class Sheep : Animal151 {152 string myName;153 public Sheep(string name): base(name)154 {155 myName = name;156 }157 ///158 /// 名字(重写父类属性)159 /// 160 public override string MyName161 {162 get { return "我是:羊羊,我叫:" + this.name; }163 }164 ///165 /// 叫(重写父类方法)166 /// 167 public override string getShoutSound()168 {169 return "咩!";170 }171 }172 173 ///174 /// 喜羊羊(子类)175 /// 继承 Sheep和接口ISpeak176 /// 177 class PleasantSheep : Sheep, ISpeak178 {179 string myName;180 public PleasantSheep(string name) : base(name)181 {182 myName = name;183 }184 ///185 /// 名字(重写父类属性)186 /// 187 public override string MyName188 {189 get { return "我是:喜羊羊,我叫:" + this.name; }190 } 191 ///192 /// 实现接口ISpeak的成员Speak193 /// 194 public void Speak()195 {196 Console.WriteLine("我会说人话:“你好,我叫:" + this.name + "~~~”");197 }198 }
5.1基类约束
这个比较常见,就是约束类型的实参都必须继承同一基类
我们新建一个泛型类CatShout,约束它的基类实参为Cat类
//泛型约束 - 基类约束class CatShoutwhere T : Cat{ public void Shout(T cat) { cat.Shout(); }}
调用及结果:
//泛型约束- 基类约束CatShoutcat = new CatShout ();cat.Shout(new BlueCat("兰喵"));Console.ReadLine();//CatShout dog = new CatShout (); 假如使用 Dog类实例化,约束生效,程序报错
5.2 接口约束
这次我们建立一个泛型类AnimalSpeak类,约束它派生自接口ISpeak
//泛型约束 - 接口约束class AnimalSpeakwhere T : ISpeak{ public void Speak(T t) { t.Speak(); }}
调用及结果:
//泛型约束- 接口约束AnimalSpeakanimalSpeak = new AnimalSpeak ();animalSpeak.Speak(new BlueCat("兰喵"));AnimalSpeak animalSpeak2 = new AnimalSpeak ();animalSpeak2.Speak(new PleasantSheep("肥咩"));//假如使用以下调用,约束生效,程序报错,因为Cat并不继承接口ISpeak,不符合接口约束//AnimalSpeak animalSpeak3 = new AnimalSpeak ();//animalSpeak2.Speak(new Cat("阿狸"));
说明:可以同时约束为类和接口,类在前面,接口在后。接口可以有多个,类只能一个,不可以继承多个类。
6.0 要点:
最后总结一下
A.泛型基本的使用比较简单,简单说就是一个取代数组和集合的安全类型
B.泛型包括,泛型类、方法、接口、委托、结构。
C.泛型在某些情况下为了调用更加安全,即在编译阶段就进行校验,经常使用约束
D.泛型的类型约束有几个方面:类约束,接口,构造函数,结构等。基类约束、接口约束和构造函数约束较为常见
E.类的约束使用where关键词,约束的几个方面有先后顺序,通常类,接口,构造函数,这样的顺序。
如果感觉约束这块儿有点难理解,小伙伴们先掌握好基本泛型使用方法。
==============================================================================================
<如果对你有帮助,记得点一下推荐哦,有不明白的地方或写的不对的地方,请多交流>
==============================================================================================