泛型是 2.0 版 C# 语言和公共语言运行库 (CLR) 中的一个非常重要的新功能,其主要作用是提高代码的重用性与灵活性。在开发的过程当中可能会遇到这样一种情况:
我们创建了一个方法名字叫add,其功能是实现两个int类型整数进行相加。代码如下:
public int add(int a, int b)//声明一个加法函数
{
return a + b; //返回两个数相加的结果
}
这个方法非常简单,就是一个普通的加法运算,但是这个方法有一个局限性,那就是他只能支持int类型的计算,而不能支持其他类型的数据相加,比如float。现在需要让这个方法也能计算float类型的话我们只能在重新重载一个。代码如下
public int add(float a, float b)//声明一个加法函数
{
return a + b; //返回两个数相加的结果
}
这样一来我们就会发现一个问题,同样的功能如果我们需要传入不同类型的数据,我们需要不停的重新写一个方法,这样一来代码的重用性会非常低下。
这是有人说了,如果我用object类不就可以了,因为object类是所有类的基类,这样一来便可以实现任何数据的传输例如
public int add(object a, object b)//声明一个加法函数
{
return decimal.Parse(a)+decimal.Parse(b);
}
这个方式确实可是实现传递各种类型的,但是有几个比较明显的缺点。那就是使用object会出现装箱、折箱操作,这将在托管堆上分配和回收大量的变量,若数据量大,则性能损失非常严重。下面我们来简要说明一下装箱于拆箱:装箱是将值类型转为引用类型,拆箱是将引用类型转为值类型。举一个例子:
static void Main(string[] args)
{
int a = 1; //定义一个变量值为1
object obj = a;//装箱的过程,将值类型的a变为引用类型obj
int m=(int)obj;//拆箱过程,将引用类型obj变为值类型m
Console.WriteLine(obj);
Console.ReadKey();
}
我们根据上面这个简单的例子简单了解一下装箱与拆箱的内部操作。c#的内存分配分为两种,第一种是栈,另一种是堆,值类型会在栈中分配内存(int float long double等等),而引用类型会在堆里面分配内存(托管堆)。 在装箱的过程当中会分为这样几个步骤:
第一步:为新生成的引用对象在堆中分配内存,大小即为值类型的大小;
第二部:将值类型的数据拷贝给引用类型;
第三部:获取引用类型的地址;
这么看来可能比较抽象,我们结合上面的例子来具体说明一下:
object obj其实就是第一步里面所讲的建立一个引用对象;然后 obj=a其实就是obj首先会在堆内存中分配和a相等的内存,然后再把a中的数据拷贝到obj,最后只需要获取引用类型的地址即可。以上就是整个装箱的过程。
拆箱过程分为这样几个步骤:
第一步:找到引用类型在堆中的值类型地址;
第二步:根据堆中值类型的地址以及大小将其复制到新建的值类型中;
(int)obj就是寻找堆中值类型的地址,int m=(int)obj 这个赋值的过程也就是根据堆中值类型的地址进行数据复制的过程。我们从拆箱和装箱可以看出来,这两个步骤都需要给引用类型分配内存,而且都有一个数据复制的过程,这就是之前所说的如果全部使用object 会带来的性能问题。为了解决这样的问题,c#出现了泛型这样一个概念。泛型可以理解为他是所有类型爸爸,他可以接受任何类型。接下来举一个例子:
class tools
{
public void add<T>(T a)
{
Console.WriteLine(a);
}
}
这是一个最简单的泛型使用的例子,他的作用是给方法传递一个值,然后显示在命令行中。这个方法中<>括号就代表这泛型,T是泛型的名称,这个名称可以又下划线和英文组成自己可以随便定义。上面这个方法可以实现任意类型的输入,我们只需要再调用的时候指明需要的类型即可:
static void Main(string[] args)
{
tools tl = new tools();
tl.add<int>(1);//传入int类型
tl.add<String>("啦啦啦")//传入float类型
}
调用的方法简单,如果理解起来还是有问题,可以这样来理解,泛型只不过是定义一个通用的类型,然后再调用的时候指定这个类型是什么。用下面这个行代码来举例:
tl.add<int>(1)
方法在调用过程中指明了泛型为int类型,然后在方法体可以理解为所有的T泛型将都变为int
public void add(int a)
{
Console.WriteLine(a);
}
如此一来我们只需要在调用方法的时候输入类型的名称即可实现参数类型的动态化,节省很多的代码量,并且提高代码的灵活性。
下面把上面这个例子变得复杂一些,如果一个方法有多个参数,并且多个参数的类型还不相同,我们应该如何处理?其实也非常简单,只需要再定义的时候多定义几个泛型然后用逗号隔开即可。
class tools
{
public void add<T,P,V>(T a,T b,P c,V d)
{
Console.WriteLine(a);
Console.WriteLine(b);
Console.WriteLine(c);
Console.WriteLine(d);
}
}
这个方法里面定义了三种泛型T,P,V。但是这个方法的参数有四个,其中参数a,b都是泛型T这表明a与b两个参数的类型是保持一致的,参数c,d有可能是别的参数类型。方法调用方式和上面是一样的,只需要为T,P,V三种泛型指定好类型就可以了
static void Main(string[] args)
{
tools tl = new tools();
tl.add<int,String,double>(1,2,"啦啦啦",35.1);//a b为int c为string d为double
tl.add<int,int,String>(1,2,3,"啦啦啦");//a b为int c为int d为String
}
这样的话调用方式就会非常灵活。
再复杂一些,将泛型引用到类上面:
class pro<T,U>
{
private T numberOne;
private U numberTwo;
//构造函数
public pro(T num1,U num2){
this.numberOne = num1;
this.numberTwo = num2;
}
public void print()
{
Console.WriteLine(numberOne);
Console.WriteLine(numberTwo);
}
}
理解起来也非常好理解,其实就是让类里面的成员与方法的类型统一了起来,调用方式如下;
static void Main(string[] args)
{
pro<String, int> proobj = new pro<String, int>("1", 1);
proobj.print();
}
同样泛型也可以应用到接口上:
interface method<T,U>
{
T add(T a, T b);
void print(U c);
}
class domethod : method<int,String>//实现接口并且指定数据类型
{
public int add(int a, int b)
{
return a + b;
}
public void print(String c)
{
Console.WriteLine(c);
}
}
在实现接口的时候需要注意实现方法的时候只有指定了泛型中的类型才可以实现方法,通过方法来指定类型是不行的,
interface method<T,U>
{
T add(T a, T b);
void print(U c);
}
class domethod : method //这里没有指定泛型的类型会报错!!
{
public int add(int a, int b)
{
return a + b;
}
public void print(String c)
{
Console.WriteLine(c);
}
}
上面这些个例子的泛型都是值类型,那么可以不可以传递引用类型? 答案是可以的,接下来我们继续改造上面这个方法,我们将接口的参数作为对象作为对象传递进去。来看下面的例子
//设置a与b的类 也就是加法参数
class setAdd_AB
{
public int a;
public int b;
public int B
{
get { return b; }
set { b = value; }
}
public int A
{
get { return a; }
set { a = value; }
}
}
//设置c的类 print参数
class setPrint
{
public String c;
public String C
{
get { return c; }
set { c = value; }
}
}
interface method<T, U>
{
int add(T AB_obj);//参数需要传递一个设置了a 与b 数值的对象
void print(U C_obj);//参数需要传递一个设置了字符串c的对象
}
class domethod : method<setAdd_AB, setPrint>//这个泛型表示传入的参数都是引用类型,都是这些类的对象
{
public int add(setAdd_AB addobj)
{
return addobj.a + addobj.b;
}
public void print(setPrint printobj)
{
Console.WriteLine(printobj.c);
}
}
这个例子就稍微复杂了一点儿,在接口实现的时候泛型是两个引用类型,也就是说,参数是一个对象。调用方式如下
static void Main(string[] args)
{
setAdd_AB Set_ab = new setAdd_AB();
Set_ab.a = 1;
Set_ab.b = 2;
setPrint Set_c = new setPrint();
Set_c.c = "啦啦啦";
domethod mth = new domethod();
mth.print(Set_c);
}
上面这个例子我们把泛型接口的泛型更改为引用类型,这样一来在实现类中可以处理任何对象的信息,灵活性提高了很多。
class domethod : method<setAdd_AB, setPrint>
但是问题又来了,既然泛型可以接受任何的数据类型,如果在开发过程中本应该设置成int类型,却写成了其他类型会不会出现问题,答案是肯定的,但是这种问题编译起来不会报错,问题一般会出现在运行的结果上。所有为了避免类似的种种问题,泛型提出了约束的概念。
泛型约束
在定义泛型类时,可以对客户端代码能够在实例化类时用于类型参数的类型种类施加限制。如果客户端代码尝试使用某个约束所不允许的类型来实例化类,则会产生编译时错误。这些限制称为约束格式为:
class class-name<type-param> where type-param:constraints{},约束分为以下几类
类型的约束:
T:结构 | 类型参数必须是值类型。可以指定除 Nullable 以外的任何值类型。有关更多信息,请参见使用可空类型(C# 编程指南)。 |
T:类 | 类型参数必须是引用类型,包括任何类、接口、委托或数组类型。 |
T:new() | 类型参数必须具有无参数的公共构造函数。当与其他约束一起使用时,new() 约束必须最后指定。 |
T:<基类名> | 类型参数必须是指定的基类或派生自指定的基类。 |
T:<接口名称> | 类型参数必须是指定的接口或实现指定的接口。可以指定多个接口约束。约束接口也可以是泛型的。 |
T:U | 为 T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数。这称为裸类型约束。 |
T:结构约束
结构约束就是说白了就是让T接受int ,float,或者结构体等等值类型,如果传入引用类型就会报错。下面我们举个例子
class structs<T> where T:struct //t为结构类型
{
public void print(T a)
{
Console.WriteLine(a);
}
}
接下来我们去调用这个类
static void Main(string[] args)
{
//正确
structs<int> st = new structs<int>();//int为值类型,符合约束,没问题
st.print(1);
//错误
structs<String> st1 = new structs<String>();//String为引用类型,不符合约束,编译报错
st.print(1);
}
T:类
引用类型约束说白了就是在设定数据类型的时候其必须是一个类,接口,委托,是一个比较大的概念,这里以基类约束为例
某些类型不允许作为基类约束:Object、Array 和 ValueType。 在 C# 7.3 之前,Enum、Delegate 和 MulticastDelegate 也不允许作为基类约束
//工具类
class basclass
{
public void show()
{
Console.WriteLine("拉拉啦");
}
}
//泛型类
class classT<T> where T : basclass //首先baseclass是引用类型,其次基类约束指定了T的约束只能为baseclass,如果为这里写的是class
则接受所有引用类型参数
{
public void print(T a)
{
a.show();
}
}
调用方式如下
static void Main(string[] args)
{
classT<basclass> ct=new classT<basclass>;//调用的时候如果泛型换成别的类就会报错
ct.print();
}
这里还有一点就是接口约束,就是指定一个类必须要实现的接口,一个类可以有多个接口约束,使用方式和基类约束是一样的。
看一个比较复杂的例子
static void Main(string[] args)
{
setPrint set_c=new setPrint();
set_c.c="1";
domethod<method<setAdd_AB, setPrint>> dos = new domethod<method<setAdd_AB, setPrint>>();
dos.print(set_c);
}
}
//设置a与b的类 也就是加法参数
class setAdd_AB
{
public int a;
public int b;
public int B
{
get { return b; }
set { b = value; }
}
public int A
{
get { return a; }
set { a = value; }
}
}
//设置c的类 print参数
class setPrint
{
public String c;
public String C
{
get { return c; }
set { c = value; }
}
}
interface method<T, U>where T:setAdd_AB where U:setPrint
{
int add(T AB_obj);//参数需要传递一个设置了a 与b 数值的对象
void print(U C_obj);//参数需要传递一个设置了字符串c的对象
}
class domethod<T> where T : method<setAdd_AB, setPrint> //这个泛型表示
{
public int add(setAdd_AB addobj)
{
return addobj.a + addobj.b;
}
public void print(setPrint printobj)
{
Console.WriteLine(printobj.c);
Console.ReadKey();
}
}
这是一个指定了模板类domethod中泛型约束T为一个接口约束,但是接口method中还有基类约束,指定了接口中T,U只能为
setAdd_AB与setPrint引用类型,
由于这里面存在一个嵌套关系所以在声明对象的时候会比较麻烦domethod<method<setAdd_AB, setPrint>>这一行代码首先指定了满足domethod约束的接口method然后method的泛型指定了满足其约束的类setAdd_AB与setPrint。
构造函数约束new()
构造函数约束就是让类型参数具有无参数或者默认的构造函数(public)。
//构造约束
class gouzao<T> where T:new()
{
T t=new T();
}
class gouzao1
{
public gouzao1()
{
}
}
class gouzao2
{
gouzao2()
{
}
}
对比一下gouzao1与gouzao2的构造函数一个有public一个没有,这时候再调用的时候约束就会起作用
static void Main(string[] args)
{
//正确
gouzao<gouzao1> gz1 = new gouzao<gouzao1>();
//编译器报错:必须有public构造函数
gouzao<gouzao2> gz2 = new gouzao<gouzao2>();
}
裸类型约束
用作约束的泛型类型参数称为裸类型约束。当具有自己的类型参数的成员函数需要将该参数约束为包含类型的类型参数时,裸类型约束很有用。
class List<T>
{
void Add<U>(List<U> items) where U : T {/*...*/}
}
泛型类的裸类型约束的作用非常有限,因为编译器除了假设某个裸类型约束派生自 System.Object 以外,不会做其他任何假设。在希望强制两个类型参数之间的继承关系的情况下,可对泛型类使用裸类型约束。
以上为泛型的基本内容,有错误欢迎指正。