.net中的泛型全面解析
从2.0起我们一直就在谈论泛型,那么什么是泛型,泛型有什么好处,与泛型相关的概念又该怎么使用,比如泛型方法,泛型委托。这一篇我会全面的介绍泛型。
那么首先我们必须搞清楚什么是泛型,泛型其实也是一种类型,跟我们使用的int,string一样都是.net的类型。泛型其实就是一个模板类型,万能类型。它允许我们在设计类的时候使用一个类型空白。预留一个类型。等到我们使用这个类的时候,我们可以使用特定的类型来替换掉我们预留的这个类型。这就是泛型。
那么这样使用的好处有什么呢?
1,类型安全性
2,性能提高
3,代码重用
4,扩展性
为什么会有这几个好处,我们来解析一下。
在我们讨论泛型的优点的时候,先来看看怎么使用泛型,泛型一般与集合一起使用。但是我们也可以创造自己的泛型类。这里我们定义一个类Person。这个类有3个变量,ID,FirstName,LastName.FirstName和LastName的类型很确定就是string。而ID的类型我们却不确定,这里的不确定是为了更好的扩展性,而不是说不能确定,比如ID可以是纯int的格式,比如1,2.同时也可以是string的ET001,ET002.当然我们可以通过拼接字符串来完成这个的操作,但是如果我们使用泛型,就能实现很好的扩展性,性能,安全性。类如下如下。
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我们可以在以后指定实际类型来替换这个类型。其实有点像委托。只不过委托预留的是一个具有特定签名的方法抽象。而泛型预留的是一个类型。这就足以说明面向对象其实从某种角度来说就是面向抽象而不是面向具体的实现。
这里我们定义的泛型类型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是现在的容量