从2.0起咱们一直就在谈论泛型,那么什么是泛型,泛型有什么好处,与泛型相关的概念又该怎么使用,好比泛型方法,泛型委托。这一篇我会全面的介绍泛型。数据库
那么首先咱们必须搞清楚什么是泛型,泛型其实也是一种类型,跟咱们使用的int,string同样都是.net的类型。泛型其实就是一个模板类型,万能类型。它容许咱们在设计类的时候使用一个类型空白。预留一个类型。等到咱们使用这个类的时候,咱们可使用特定的类型来替换掉咱们预留的这个类型。这就是泛型。数组
那么这样使用的好处有什么呢?安全
1,类型安全性ide
2,性能提升函数
3,代码重用性能
4,扩展性测试
为何会有这几个好处,咱们来解析一下。this
在咱们讨论泛型的优势的时候,先来看看怎么使用泛型,泛型通常与集合一块儿使用。可是咱们也能够创造本身的泛型类。这里咱们定义一个类Person。这个类有3个变量,ID,FirstName,LastName.FirstName和LastName的类型很肯定就是string。而ID的类型咱们却不肯定,这里的不肯定是为了更好的扩展性,而不是说不能肯定,好比ID能够是纯int的格式,好比1,2.同时也能够是string的ET001,ET002.固然咱们能够经过拼接字符串来完成这个的操做,可是若是咱们使用泛型,就能实现很好的扩展性,性能,安全性。类以下以下。spa
public class Person<T> { private T _t; public T T1 { get { return _t; } set { _t = value; } } private string _firstName; public string FirstName { get { return _firstName; } set { _firstName = value; } } private string _lastName; public string LastName { get { return _lastName; } set { _lastName = value; } } public Person() { } public Person(T t1, string firstName, string lastName) { this._t = t1; this._firstName = firstName; this._lastName = lastName; } }
泛型类的定义是很简单的<T>,这样就能够定义泛型类,这里咱们使用了泛型T,预留了一个类型。泛型所能理解的操做是:1这里是一个类型,2这个设计时未知,3咱们能够在之后指定实际类型来替换这个类型。其实有点像委托。只不过委托预留的是一个具备特定签名的方法抽象。而泛型预留的是一个类型。这就足以说明面向对象其实从某种角度来讲就是面向抽象而不是面向具体的实现。.net
这里咱们定义的泛型类型T,就能够在后续使用时使用不一样的类型来替换。这里就能够作到咱们前面提到的使用int,或者string,或者其余的任何咱们想要的类型,甚至是咱们本身定义的类型。咱们来看看调用代码。
Person<int> person = new Person<int>(1, "Edrick", "Liu"); Person<string> personString = new Person<string>("ET001", "Edrick", "Liu"); Console.WriteLine("INT:ID:{0},FirstName:{1},LastName:{2}",person.T1,person.FirstName,person.LastName); Console.WriteLine("STRING:ID:{0},FirstName:{1},LastName:{2}",personString.T1,personString.FirstName,personString.LastName);
这里咱们不须要拼接字符串,不须要作任何额外的操做就能够实现。
这里咱们说明了代码重用性。
咱们能够扩展类型T,在任什么时候候,若是需求发生了变化,又要以不一样的格式来输出ID。咱们甚至能够扩展一个ID类。而后用ID类来替换T。
public class MyID { private string _city; public string City { get { return _city; } set { _city = value; } } private string _school; public string School { get { return _school; } set { _school = value; } } private string _className; public string ClassName { get { return _className; } set { _className = value; } } private string _number; public string Number { get { return _number; } set { _number = value; } } public MyID() { } public MyID(string city, string school, string className, string number) { this._city = city; this._school = school; this._className = className; this._number = number; } }
咱们扩展了一个ID类,用这个复杂类型来用做咱们的ID。这里咱们不须要更改Person类就能够直接扩展ID了。由于T是可使用任何类型来替换的。
MyID myId =new MyID("WuHan", "SanZhong", "YiBan", "0001"); Person<MyID> personID = new Person<MyID>(myId, "Edrick", "Liu"); Console.WriteLine("ID:{0},FirstName:{1},LastName:{2}",myId.City+"-"+myId.School+"-"+myId.ClassName+"-"+myId.Number,personID.FirstName,personID.LastName);
这里说明了扩展性
固然有人会说,你这里泛型能够作到的,咱们用object也一样能够作到,是的,这里泛型能够作到的,object也一样能够作到。可是咱们来看下一个实例。这里咱们使用ArrayList来作这个示例。
ArrayList list = new ArrayList(); list.Add(1); list.Add(2); list.Add(3); IEnumerator ie = list.GetEnumerator(); while (ie.MoveNext()) { int i = (int)ie.Current; Console.WriteLine(i); }
很简单的一个示例。示例话一个ArrayList,而后添加3个数字。而后枚举。这里我为何要使用枚举而不直接foreach呢,这样咱们更能直接看清楚使用object的时候类型之间的转换。若是不清楚foreach为何能够以这样的代码替换的,能够参考个人迭代器一文。
咱们来看一幅图。
这就是咱们往集合里面添加元素时候的提示,咱们能够看到类型是object。若是咱们往里面加入int型元素,那么元素天然会被装箱。那么在咱们迭代的时候呢?上面的代码显示了有一个强制转换,就是拆箱了。因此这里进行了一次装箱和拆箱。装箱和拆箱是会有性能损失的,园子里也有朋友作过测试。http://archive.cnblogs.com/a/2213803/就作了一个测试,你们能够看看。这里咱们须要知道的就是使用集合其实是发生了装箱和拆箱。那么还有一个问题也就出来了,既然这里咱们可使用int,固然也能够加入string类型的元素。由于他们均可以成功的转换为object,由于object是最终父类。因此如下代码也是能够经过编译的。
ArrayList list = new ArrayList(); list.Add(1); list.Add(2); list.Add(3); list.Add("e");
这段代码毫无疑问的能够经过运行的,可是咱们在迭代的时候就会出问题了。很明显(int)e.这个强制转换是不能成功的。编译器期间无错误而错误发生在运行期。这对咱们来讲是不但愿看到的,那么泛型的处理方式呢?
这里咱们能够看到,咱们使用的是int类型替换的类型T,因此咱们在add的时候就只能add替换T的int类型,而不是想非泛型的任何类型均可以add。
因此这里既说明了性能和安全性
这里有一个问题须要注意如下,咱们在声明泛型T的时候,并非必定类型名是T,T是在一个类型的时候,若是咱们须要使用多个泛型来实例化一个类型,那么咱们就须要使用说明性的名称,好比TId,TFirstName之类的。
public class PerosnMoreTypeOfT<TId,TFirstName,TLastName> { private TId _id; public TId Id { get { return _id; } set { _id = value; } } private TFirstName _firstName; public TFirstName FirstName { get { return _firstName; } set { _firstName = value; } } private TLastName _lastName; public TLastName LastName { get { return _lastName; } set { _lastName = value; } } public PerosnMoreTypeOfT() { } public PerosnMoreTypeOfT(TId tId, TFirstName tFirstName, TLastName tLastName) { this._id = tId; this._firstName = tFirstName; this._lastName = tLastName; } }
调用代码
PerosnMoreTypeOfT<int, string, string> person = new PerosnMoreTypeOfT<int, string, string>(1, "Edrick", "Liu"); Console.WriteLine("ID:{0},FirstName:{1},LastName:{2}",person.Id,person.FirstName,person.LastName);
这是须要注意一下的。
泛型类型的约束
所谓的泛型类型约束,实际上就是约束的类型T。使T必须遵循必定的规则。好比T必须继承自某个类,或者T必须实现某个接口等等。那么怎么给泛型指定约束?其实也很简单,只须要where关键字。加上约束的条件。
约束条件有如下
where T : struct -类型T必须是值类型
where T : class -类型T必须是引用类型
where T : Ifoo -类型T必须执行接口Ifoo
where T : foo -类型T必须继承自 foo
where T : new() -类型T必须有一个默认构造函数
where T : U -指定泛型类型T1派生于T2。
下面我会解释每一个约束该怎么用,使用约束不仅仅能够限制T,并且还可使T具备类型可用性,上面咱们介绍了,咱们只有在实例化的时候才替换泛型类型,因此咱们除了能把泛型转换为object外,基本上在定义的时候不能与其余类型作任何交互,若是这里我约束泛型T实现了接口IFoo,那么咱们就能够把泛型转换为IFoo,从而使用Ifoo里定义的方法。这样就使类型在定义的时候就可使用,而不须要等到实例化。
而指定T的类型是很是简单的。
public class Person<T>where T:struct
这时,若是咱们使用引用类型替换T就会编译出错。咱们也能够约束T为引用类型,这里写一个例子,怎么使约束为接口和基类型,而后使用这些类型。
public interface IPerson { void DisplayPerosnWithOutId(); void DisplayPerosnWithId(); }
定义接口。
public class Person<T>:IPerson where T:MyID { private T _t; public T T1 { get { return _t; } set { _t = value; } } private string _firstName; public string FirstName { get { return _firstName; } set { _firstName = value; } } private string _lastName; public string LastName { get { return _lastName; } set { _lastName = value; } } public Person() { } public Person(T t1, string firstName, string lastName) { this._t = t1; this._firstName = firstName; this._lastName = lastName; } public void DisplayPerosnWithOutId() { Console.WriteLine("FirstName:{0},LastName:{1}",FirstName,LastName); } public void DisplayPerosnWithId() { MyID myId = T1; Console.WriteLine("ID:{0},FirstName:{1},LastName:{2}", myId.City + "-" + myId.School + "-" + myId.ClassName + "-" + myId.Number, FirstName, LastName); } }
这里让咱们的Perosn实现这个接口,而后咱们的Perosn里面的泛型T必须是继承自MyId的。注意,这里约束的是咱们的Person的T
public class DisPerson<T>where T:IPerson { private T t { get; set; } public DisPerson() { } public DisPerson(T t1) { this.t = t1; } public void dis() { IPerson p = (IPerson)t; p.DisplayPerosnWithId(); p.DisplayPerosnWithOutId(); } }
这里就是咱们的泛型类,这个类的约束是T必须实现IPerson。因此T就能够跟IPerson实现转换,从而调用IPerosn里调用的方法。
MyID myId = new MyID("WuHan", "SanZhong", "YiBan", "0001"); Person<MyID> perosn = new Person<MyID>(myId,"Edrick","Liu"); DisPerson<Person<MyID>> dis = new DisPerson<Person<MyID>>(perosn); dis.dis();
这里使用到了2种约束,而值类型约束跟引用类型约束是很简单的,咱们只须要where一下。下面来看看U约束。代码很简单
public class ClassA { } public class ClassB:ClassA { } public class ClassC<TClassA,TClassB> where TClassB:TClassA { }
这里ClassB必须是继承是ClassA。
ClassA a = new ClassA(); ClassB b = new ClassB(); ClassC<ClassA, ClassB> c = new ClassC<ClassA, ClassB>();
若是这里咱们的ClassB不继承自ClassA,那么编译将不能经过。
Default关键字
default关键字其实不须要解释太多,这里只解释一下原理就好了。咱们前面提到,泛型只是一个模板类型,就是咱们在定义的时候根本就不可能知道用户在实例化的时候会以何种类型来替换。有能够是值类型,也有多是引用类型。值类型是不能赋值为null的。因此泛型类型不能赋值为null,可是这里仍然有50%的概率是引用类型,咱们仍是须要50%的机会须要泛型T为null。这时就须要default关键字。
private T t =default(T);
这里就能够避免咱们上面所说的问题,这里会有两种状况。一种是若是T为值类型,则赋值0,若是T为引用类型则赋值为null。
泛型类的静态成员
泛型类的静态成员跟咱们平时处理静态成员有些许不一样。一段代码就能够解释清楚。
StaticGeneric<int>.x = 5; StaticGeneric<int>.x = 7; StaticGeneric<string>.x = 6; Console.WriteLine(StaticGeneric<int>.x); Console.WriteLine(StaticGeneric<string>.x);
输出的是7和6.使用不一样的类型替换泛型获得的是不一样的类实例。
泛型继承和泛型接口
如今咱们来看看泛型的继承和泛型接口。咱们先来看看泛型继承。
类能够继承自泛型基类,泛型类也能够继承自泛型基类。有一个限制,在继承的时候,必须显示指定基类的泛型类型。咱们来看看示例
public abstract class Base<T>where T:struct { private int _id; public int Id { get { return _id; } set { _id = value; } } public abstract T Add(T x, T y); }
一个抽象类,定义了一个ID属性,定义了一个抽象方法。下面是继承类
public class SonClass<T>:Base<int> { private T _t; public T T1 { get { return _t; } set { _t=value;} } public SonClass() { } public SonClass(T t,int id) { this._t = t; base.Id = id; } public override int Add(int x, int y) { return x + y; } public void Prit() { Console.WriteLine(T1); } }
实现了基类里面的抽象方法,自己实现了一个方法,这里的T跟咱们的基类的泛型类型没有任何关系。调用代码
SonClass<string> son = new SonClass<string>("EdrickLiu",10); Console.WriteLine(son.Id); Console.WriteLine(son.Add(3,5)); son.Prit();
其实跟咱们的非泛型继承没有多少太大的区别。那么,泛型接口呢?其实也很简单。咱们定义一个接口ICompare<T>接口,这个接口很简单,按值比较对象。其实跟继承大同小异
public interface ICompare<T> where T:class { bool CompareTo(T one,T other); }
这个接口很简单,定义了一个方法,比较两个对象,而后咱们实现这个接口
public class CompareClass:ICompare<MyID> { public bool CompareTo(MyID one, MyID other) { if (one != null && other != null) { if (one.City == other.City && one.School == other.School & one.ClassName == other.ClassName && one.Number == other.Number) { return true; } else { return false; } } else { return false; } } }
这里跟继承同样,实现接口的时候须要制定泛型类型。而后咱们就能够调用了
MyID id = new MyID("Wuhan", "Shanzhong", "YiBan", "ET001"); MyID myid = new MyID("Wuhan", "Shanzhong", "YiBan", "ET002"); ICompare<MyID> compare = new CompareClass(); Console.WriteLine(compare.CompareTo(id, myid));
其实有了这个方法,咱们就能够不须要重载运算符或者重载Equals了。下面咱们来看看泛型泛型方法和泛型委托,当初在写委托的时候我在考虑泛型委托要放在什么地方写,最后仍是放在这里了。
泛型方法&泛型委托
泛型方法其实跟泛型类差很少,方法在定义的时候使用泛型类型定义参数。调用的时候使用实际类型替换。这样就可使用不一样的类型来调用方法,咱们先来看一个简单的,交换两个数。能够是int也能够是double,或者别的类型。
public static void Swap<T>(ref T x,ref T y) { T temp; temp = x; x = y; y = temp; }
这里使用int是由于咱们要改变x,y的值,x,y都是值类型,因此要调用ref。调用代码就很简单了
int x = 8; int y = 9; GenericMethod.Swap<int>(ref x,ref y); Console.WriteLine("X:{0},Y:{1}",x,y);
咱们这里也能够省略<int>,写成GenericMethod.Swap(ref x,ref y);编译器会本身判断。这只是一个很简单的方法,前面咱们说过泛型与集合一块儿使用会很强大,咱们来看一个泛型方法与集合一块儿使用的例子。咱们有一个实体类Person,它有3个字段,ID,Name,Salary.咱们要实现的功能就是自动计算总的薪水。首先定义实体。
public class SalaryPerson { private int _id; public int Id { get { return _id; } set { _id = value; } } private string _name; public string Name { get { return _name; } set { _name = value; } } private decimal _salary; public decimal Salary { get { return _salary; } set { _salary = value; } } public SalaryPerson() { } public SalaryPerson(int id, string name, decimal salry) { this._id = id; this._name = name; this._salary = salry; } }
而后再咱们刚刚的泛型方法类里加入方法
public static decimal AddSalary(IEnumerable e) { decimal total = 0; foreach (SalaryPerson p in e) { total += p.Salary; } return total; }
这里就有一个问题了,咱们只能SalaryPerson类型了,那么这里咱们就能够引入泛型了,泛型的做用就是在咱们不肯定类型的时候作一个替换类型,因此咱们这里就可使用泛型了。更改事后的方法是
public static decimal AddSalary<T>(IEnumerable<T> e) where T:SalaryPerson { decimal total = 0; foreach (T t in e) { total += t.Salary; } return total; }
咱们在前面说到了,若是要在定义T的时候使用T,就应该使它继承于或者是某类,或者实现某个接口。可是咱们这里仍是只能计算SalaryPerson或者派生于SalaryPerson的类,咱们能不能计算别的类,计算的逻辑由咱们定义呢?固然能够,就是泛型委托。
泛型委托
我想详细的讲讲泛型委托,由于我以为自从3.0以后泛型委托是用得愈来愈多,泛型委托与lamda也是越用越多,lamda表达式我在委托一文中讲到了。委托的概念我也讲了,因此这里我不过多的讲述什么是委托。委托是能够引用方法的,只要方法签名符合,好比一个很简单的方法签名public int Add(int x,int y).这里咱们须要注意两点。一点是返回类型,一点是参数。若是咱们须要定义的只是一个功能,可是功能的实现要到具体的地方才能肯定,咱们就可使用委托,可是使用委托咱们的方法返回值和参数类型就肯定了,咱们可让委托具备更高等级的抽象,返回值,参数类型都到具体的地方制定。这里的具体地方就是咱们要实现的方法。这样,咱们的委托就具备更高级别的抽象。咱们设计的类就具备更高级别的能够用性,咱们只须要实现方法的细节就能够了。方法的细节怎么实现,可使用匿名方法,或者lamda表达式。下面咱们来看看在具体的代码中咱们该怎么实现。继续咱们上面的那个例子。首先定义一个类GenericInFramerwork
public delegate TResult Action<TInput,TResult>(TInput input,TResult result); public static TResult GetSalary<TInput, TResult>(IEnumerable<TInput> e, Action<TInput, TResult> action) { TResult result =default(TResult); foreach (TInput t in e) { result = action(t,result); } return result; } }
这个类里面定义了一个泛型委托,委托定了两个参数,一个是返回类型,一个操做类型。这里解释一下参数为何要加上返回类型,由于咱们不能用通常的算术运算符来操做泛型类型,+=是不容许的,因此这里只能使用result=action(t,result)那么咱们就须要一个返回值来保持传递咱们的 result.调用代码
List<SalaryPerson> list = new List<SalaryPerson>(); list.Add(new SalaryPerson(1,"Edrick",5000)); list.Add(new SalaryPerson(1,"Meci",3000)); list.Add(new SalaryPerson(1,"Jun",1000)); GenericAndDelegate.Action<SalaryPerson,Decimal> a = new GenericAndDelegate.Action<SalaryPerson,Decimal>(GetS); decimal d = GenericAndDelegate.GetSalary<SalaryPerson, Decimal>(list, a); Console.WriteLine(d); Console.Read(); } public static decimal GetS(SalaryPerson p,decimal d) { d += p.Salary; return d; }
首先实例化委托,而后调用咱们的泛型方法。这里就是为何参数要定义返回类型,这里若是咱们去掉参数,而在GetS方法里定义一个局部变量,那么结果是咱们只能获得最后意项的结果。相比上面的一个例子,这里的薪水的计算逻辑彻底就是可变的,咱们能够在调用委托的时候变化咱们的逻辑,好比全部的加上200而后存进数据库,好比加上全部*10.我记得以前有人谈过一个问题,就是委托的变化过程,咱们这里使用的是最原始的委托的实例化,下面我就来归纳一下委托的实例化的发展。上面最原始个人我就不介绍了。还有
GenericAndDelegate.Action<SalaryPerson,Decimal> a = GetS;
这里就是直接把方法名称赋给委托,这是第二阶段。咱们能够能够不实例化委托,直接把方法名当作参数传递给方法。
decimal d = GenericAndDelegate.GetSalary<SalaryPerson, Decimal>(list, GetS);
再日后呢,咱们可使用匿名方法
GenericAndDelegate.Action<SalaryPerson, Decimal> a = delegate(SalaryPerson p, decimal d1) { d1 += p.Salary; return d1; }; decimal d = GenericAndDelegate.GetSalary<SalaryPerson, Decimal>(list, a);
如今呢?咱们就可使用lamda表达式了。
decimal d = GenericAndDelegate.GetSalary<SalaryPerson, Decimal>(list, (p,dl)=>dl+=p.Salary);
这里的p,dl咱们省略了类型,可是编译器会帮咱们推断,固然,你加上也是没有问题的
decimal d = GenericAndDelegate.GetSalary<SalaryPerson, Decimal>(list, (SalaryPerson p,decimal dl)=>dl+=p.Salary);
咱们这里也能够加上咱们不一样的逻辑。这里咱们就不仅仅是只能对SalaryPerson作操做了,还能对别的对象作操做。
decimal d = GenericAndDelegate.GetSalary<SalaryPerson, Decimal>(list, (SalaryPerson p,decimal dl)=>dl+=p.Salary/10);
这里的泛型委托是咱们本身的例子,上面也说了,泛型委托在咱们的.netframerwork中的应用也很普遍,咱们举两个例子,一个是在数组中,一个是在linq中,这里不介绍 Linq,我只是举例说明。
int[] ints = { 2,4,6,5,8,9,10}; Array.Sort<int>(ints, (i, j) => i.CompareTo(j)); Array.ForEach(ints, i => Console.WriteLine(i*2)); var query = ints.Where(i => i > 2); foreach (int i in query) { Console.WriteLine(i); } Console.Read();
咱们只须要找到对应的委托,而后编写lamda就能够了。
泛型类型的实例化规律
这一节主要是要咱们了解一下泛型在实例化时候的规律。咱们能够用值类型或者引用类型实例化范围,用值类型和引用类型有什么区别呢?咱们使用值类型实例化泛型,每次实例化都会创造不一样的实例,可是若是实例化的类型不一样(都是值类型),那么就会创造不一样的版本,不一样的实例,引用类型则不一样,引用类型会一直使用第一次实例化泛型时候的版本。由于值类型须要复制数据,数据的大小是不一样的,因此有不一样的版本,而引用类型只须要传递引用,因此可使用同一个版本。
List<int>和List<int>会是同个版本不一样实例,可是他们共享list<int>的单个实例。
List<int>和List<long>会创造不一样的版本,不一样的实例。这就是咱们上面说的静态值不一样的缘由。
List<string>和List<object>或创造同一版本可是实例不一样。
这些咱们理解就好了。
还有一点须要注意,泛型类型在添加整个集合的时候不支持隐式转换,好比
List<object> o = new List<object>(); List<int> iss = new List<int>(); iss.Add(1); o.AddRange(iss);
这里咱们须要显示转换一下,咱们能够写一个方法。
public void Convert<TInput,TOut>(IList<TInput> input,IList<TOut> outo) where TInput:TOut { foreach (TInput t in input) { outo.Add(t); } }
泛型这里也介绍得差很少了,.netframerwork中有些泛型,好比Nullable<T>我在可空类型介绍了,事件泛型,我会在事件中介绍。泛型跟反射我会在反射中介绍,泛型跟属性,我会在属性中介绍。最后就是一个类型了,介绍一下。ArraySegment<T>.表示数组段。直接看代码吧
int[] ints = { 2,3,4,5,6,7,8,9}; ArraySegment<int> arr = new ArraySegment<int>(ints, 2, 3); for (int i = arr.Offset; i < arr.Count+arr.Offset; i++) { Console.WriteLine(arr.Array[i]); }
Offset是相对原数组的索引,而count是如今的容量