泛型的引入
面向对象的编程很大一个好处就是可以达到代码重用,一个类继承另一个类,只需添加一些新的方法,就可以使用基类和自己的方法,很大程度上重用了代码。
那么对相似的一组操作是否可以达到这种重用的效果,比如对一个数组排序,我们需要它能针对常用的int,string,DateTime等类型通用,像c++中模板那样,调用时只需指定具体类型便可完成同一个操作过程。CLR和C#语言就提供这么一种机制-泛型(generic)
对于同一个算法,我们对不同类型进行操作时,我们要调用针对具体类型而定制的方法。有了泛型,我们只要指定一个类型,调用一个方法就好了,无论是引用类型,值类型,还是接口,委托均适用。
泛型的使用:
例如:List<T> 类型
List<int> lis = new List<int>();
lis.Add(1);
泛型使用T类型代替类型,在使用时才用实际的类型替代,这种功能需要C#语言和CLR层面配合操作。
泛型的优势
类型安全:编译器会帮我们检验类型是否对应,这对于编程来说是件好事
性能更佳:这也是最重要的一点,一种新技术的产生,无非是由易用性和高效率所决定的
在泛型没出来之前,我们想做到通用,是不是得把类型转成Object(所有类型都隐式派生自Object,都可以转化为Object类型),可以看看ArrayList等类。这种方式存在一定的缺陷
1、在转化过程中,编译器都需要做类型安全检查,这个过程是要耗费性能的
2、最可怕的是会遇到装箱与拆箱(就是值类型和引用类型之间的相互转换),了解这其中原理的人都应该清楚,这个是非常耗费性能和内存的(这个有人也做过测试,拿ArrayList和List<T>做比较,引用类型相差无几,但值类型有数十倍之差)
代码清晰:编写过程几乎不需要类型转换,代码也就更明确,简洁
代码爆炸问题
我们看看,一个泛型参数T,它可以接受不同的类型,它编译后真正运行的时候,肯定做了处理的,比如int类型和string类型的比较方式就不同,还有int16,int32最终cup命令很可能都不一样,具体操作时肯定会对应到其相应的实现方法中。
那么这么多类型,如果每个最后在运行中都生成一份,岂不是“代码爆炸”了。
其实,这里是可以优化的,拿CLR来说:
1、例如List<DateTime>类型,编译器第一次遇到,会编译一份并留着,等第二次遇到时,直接拿来用,减少重复。
2、针对引用类型(所有的引用类型的变量都指向堆中对象的地址,既然都存放的只是个地址,指向哪个不都一样,可以共享),所有的对象其实都共享了这个方法。
泛型接口和泛型委托
有了泛型,泛型接口也是少不了的。比如:FCL中的很多类型都继承自非泛型接口,当泛型继承这些非泛型接口时,会产生装箱拆箱问题,这也是性能需要。
同样是为了类型安全和避免装箱性能损失,CLR也提供了泛型委托
逆变和协变
在泛型接口和泛型委托中,其中参数类型可以允许转变,比如 <string> 转到 <object>,这时,编译器并没有给我们做好决策,默认转换,而是需要我们显示的用in和out声明如何转换才行。
逆变:in 从基类到派生 如:object到string
协变:out 从派生类到基类
这个特性主要是让代码有时能重用,比如针对不同类型写一套代码即可。
泛型中类型推断
一般方法可以使用泛型定义,以适应不同类型的操作,例如我们熟知的Swap()
在调用泛型方法时,可以省略类型,直接调用,编译器帮我们做推断
static void Swap(ref int a, ref int b)
{
Console.WriteLine("非泛型");
int temp = a;
a = b;
b = temp;
}
static void Swap<T>(ref T a, ref T b)
{
Console.WriteLine(typeof(T).ToString() + "-" + a);
T temp = a;
a = b;
b = temp;
}
static void Main(string[] args)
{
int a = 1, b = 2;
Swap(ref a, ref b); //调用的是非泛型的Swap,在泛型和非泛型同时匹配时,优先非泛型
Swap<int>(ref a, ref b); //指定类型时,就会调用泛型方法
string aa = "aa", bb = "bb";
Swap(ref aa, ref bb); //只匹配到泛型string方法
Swap<string>(ref aa, ref bb); //也只匹配到泛型string方法
}