泛型对于我来说一直是个心头大患,俗话说得好:
人类最古老、最强烈的恐惧是对未知的恐惧。
这种恐惧是全方位的,那么我们就从最开始来打破它。
泛型之所以让人害怕,主要是它太过于多变,他很像假面骑士Decade,Decade在各大剧场版和ZIO里面都很强,是因为它能变成其他的假面骑士(平成昭和都能变),你刚知道他的招数,他就变成另一个假面骑士使用其他的能力了。就跟我们的泛型一样,能和其他的类型进行融合,有泛型类、泛型委托、泛型接口、泛型结构、泛型方法。所以我们在给出定义之前,先写泛型的相关例子。
首先做一些前期准备,我们先声明一个空类,这个类不做任何用处,只是拿来做类型的测试,因为一个类就是一个类型。
public class Game
{
}
1.泛型方法
首先我们来个简单的,使用泛型方法来测试一下,我们在方法前面加入一个<T>表示我们需要使用泛型,那么,我们在调用方法的时候要声明我们的T是哪种类型。
static void Main(string[] args)
{
int a = 123;
FanType fan = new FanType();
fan.Show<int>(a);
}
}
public class FanType
{
public void Show<T>(T parameter)
{
Console.WriteLine("当前的类型是"+parameter.GetType());
}
}
那么,很显然,我们输出的结果就是:
那么,如果是泛型的那个“型”是个类呢,仍然是一样的。
我们的代码改成:
static void Main(string[] args)
{
var a = new Game();
FanType fan = new FanType();
fan.Show<Game>(a);
}
}
public class FanType
{
public void Show<T>(T parameter)
{
Game game = new Game();
if(parameter.GetType()==game.GetType())
{
Console.WriteLine("是同一类型");
}
else
{
Console.WriteLine("不是同一类型");
}
Console.WriteLine("当前的类型是"+parameter.GetType());
}
}
结果也很显然,我们在函数里面创建了一个Game的实例,两个的Type是一样的。
注意,在泛型方法调用的时候,我们可以不声明类型,传过去T类型就能给到Type类型了。
fan.Show(a);
这就是泛型方法的使用。
2.泛型类
在上面的代码中实现了泛型方法,那么接下来如果要使用泛型类呢,我们先把"<T>"紧接着放在类声明的后面试试。。。
public class FanType<T>
{
public void Foreachfan(T parameter)
{
Game game=new Game();
if (parameter.GetType() == game.GetType())
{
Console.WriteLine("是同一类型");
}
else
{
Console.WriteLine("不是同一类型");
}
Console.WriteLine("当前的类型是" + parameter.GetType());
}
}
public class FanTypeSon<T> : FanType<T>
{
public void SeeType(T t)
{
Console.WriteLine("当前类型是" + t.GetType());
}
}
这里我们在父类中定义了一个泛型,那么在子类中,我们还是可以继承它,可以有两种方式:
- public class FanTypeSon<T> : FanType<T>
- public class FanTypeSon : FanType<string>
这两种都是泛型类的继承,
第一种就是把这个泛型托付给子类,它在创建实例的时候仍然需要知道T的类型。
第二种就是表明泛型里的类型,这样子类就能知道父类方法字段中泛型字段和方法的类型是什么,然后子类直接使用就好,当然,这样自然让子类失去了使用泛型的能力。这种方法在创建的时候若是父类对象指向子类实例,所声明的类型必须与继承时类型一致,例如我上一句里面继承的时候声明了string,我理应这样写:
FanType<string> fan=new FantypeSon();
我们在上面使用了第一种方式继承,主函数中这样写:
static void Main(string[] args)
{
var Instance = new Game();
FanType<Game> fan = new FanTypeSon<Game>();
((FanTypeSon<Game>)fan).SeeType(Instance);
fan.Foreachfan(Instance);
}
注意这里,创建了父类的对象并指向子类的实例,但是由于父类中没有子类的方法,所以实例需要类型转换后才能调用子类的方法。输出结果:
3.泛型接口
泛型接口意味着,接口里面的方法的返回值、参数都有可能是泛型的,那么我们写个例子,就拿上面的改一改
interface IFanType<T>
{
void Foreachfan(T parameter);
}
public class FanTypeSon<T> : IFanType<T>
{
public void Foreachfan(T parameter)
{
Game game = new Game();
if (parameter.GetType() == game.GetType())
{
Console.WriteLine("是同一类型");
}
else
{
Console.WriteLine("不是同一类型");
}
Console.WriteLine("当前的类型是" + parameter.GetType());
}
}
如同类之间继承一样,接口的子类在继承的时候,也是可以选择是否标明泛型的类型。
主函数这样写:
static void Main(string[] args)
{
var Instance = new Game();
IFanType<Game> fan = new FanTypeSon<Game>();
fan.Foreachfan(Instance);
}
接口的实现和类继承很相似。
总结
!概念:
泛型是允许延迟编写类或方法中的编程元素的数据类型的规范,直到实际在程序中使用它的时候才被指定确定的类型。
!原理:
当我面的编译器编译的时候遇见泛型,会做特殊的处理:生成一个占位符。
当进行JIT编译的时候,会首先会获取对应的CIL代码,然后使用我们指定的类型替换掉占位符,最后再将替换后的CIL代码编译为原生代码。
!Mono对泛型的优化:
- 如果在编译的时候每一个方法+类型的组合都要生成一次原生代码,那么该方法多次调用之后的编译将会臃肿不堪,在Mono中,一个“方法+类型组合”只会编译一次,后面再次出现调用该方法时将直接复用,而不会重新编译。
- 在Mono中,所有的引用类型的实参和变量都是完全相同的,它们都被认为是指向托管堆内存中的某个对象的指针,会采用同样的方式来处理。而值类型则必须单独编译成原生代码。
!泛型的优点:
不用泛型的缺点:
在C#中,众所周知的是所有的类的基类都是Object类,那么,如果我要动态确定类型实现代码复用,我不是直接使用Object对象不就好了吗?如下图,两个函数的效果是一致的:
Object Change(Object a)
{
return a;
}
T Change<T>(T a)
{
return a;
}
而使用Object类型的弊病在于,Object类型本身是引用类型。对于引用类型和值类型的内存存放方式不一样,导致如果我们如果使用Object存放一个值类型的时候,会引发装箱操作。例如:
int a=5;
Object b=a
那么,如果说使用Object类型装填一个值类型(int),它会在托管堆中分配内存,返回的是堆中对象引用的地址。这样造成了内存不必要的损耗。而众所周知,频繁的装箱拆箱会造成不必要的性能损耗,拖慢程序的速度(频繁调用堆=》增加GC的次数)。
而泛型的优点就体现出来了:
1.类型安全:泛型中保证了类型的确定,如果类型不兼容就会报错甚至抛出异常。保证了只有与指定数据类型兼容的情况才能用于该泛型类型。
2.性能更好,在传进去参数前参数的类型就已经被动态指定好了,这样不会发生装箱拆箱操作,性能上优于使用Object作为参数或者返回值。
3.代码复用,一个类只需要一次定义,不需要多次声明逻辑相似但是类型不同的代码,实例化后的类型也变得多种多样。