泛型和协变逆变
c#中的泛型不支持特例化。所谓特例化就是针对 T 的不同实参,编写特定的代码。因此c#的泛型实际上都是一份代码,不会出现c++中的代码爆炸问题(缺点是让你的exe看起来很小)。唯一的例外是对值类型,因为值类型的大小不一致,所以需要生成新的代码,解决入栈出栈等实现问题,但逻辑上是一致的。
一、泛型的语法
二、协变和逆变
协变这个词是怎么来的?据说是来自数学专业,难怪让人摸不着脑袋。要了解协变逆变,首先要了解类型转化的过程。类型系统支持类型转换,从子类可以转换成基类类型。
假设子类有成员A,B, 父类有成员A,当子类对象转成父类,也就是客户端会把子类当作父类来用。当使用父类A成员,实际上就是使用子类的A成员,因为子类包含了父类的所有成员,而客户只能通过父类去使用子类,那么就只能使用父类有的成员,不会出现一个成员C,是子类所没有的。因此子类对象就兼容父类。
反过来就不行,虽然客户使用子类的A,父类也有,但是使用子类的成员B,父类就没有对应的成员,必然就无法执行。假设父类和子类的成员一样,当然可以相互转换,不过这是特殊情况,c#只保证子类转换成父类是安全的,至于父类转换成子类,有任何问题都由程序员自己负责。
另外一种类型转换是数值类型的转换,原则上是小范围的类型可以转成大范围的类型,这样才不丢失数据。不过,当前的C#还不支持值类型的转换规则用于泛型的协变或逆变。
协变逆变同样遵从类型转化的规则。c#用out 和 in 修饰泛型参数,支持协变逆变的泛型称为“变体”。out参数用于函数的返回值,in参数用于函数的参数。
当是out参数的时候:如 out 子类 和out 父类。假设客户使用了父类的返回类型,那么它就要求实际的返回对象自少是父类,或者基于父类的扩展(也就是子类)。假设客户使用了子类的返回类型,那么返回父类的实际对象,当调用子类中存在的成员,而不在父类中存在,必然就是一个错误。
当是in参数的时候:如in 子类和 in 父类。假设客户使用了父类的参数类型,那么他可以传子类对象实参,也能传父类对象实参进去。但是请注意,实际调用的是子类的函数!那么这个函数实际上就可能用到了子类才有的成员;如果客户把子类对象传进去当然没问题,但是如果客户把父类对象传进去,那么必然就导致错误!和上面有所不同的是,错误产生在对象内部,而不是客户段。
当使用的是子类的参数类型,客户只能传子类实参,而不能传父类实参,如果实际调用的是子类函数,当然没问题,如果实际调用的父类函数,子类实参可以转化为父类形参,也没有问题。
用代码说明:
--------------------------(1)协变
class B : A;
A fa(); B fb();
//客户
A a = fa() or fb();//ok
这里面的a 可能是A对象,也可能是B对象,但是客户端只会使用A对象的成员,因此是没有任何问题的。
B b= fa() or fb();//
假设这个也可以,那么b可能是A对象,那么客户端会通过b访问A对象中不存在的成员,因为b是B类型的变量。
原则就是:实际对象必须是自身或者后代类型,否则无法合理赋值。
所谓协变,就是实际是fb的对象,但是通过fa访问,既然是通过fa访问,那么客户端就只能是A类型,和前面陈述的情况一致,因此是可行的。
delegate T f<out T>();
f<A> fa = ()=>new B();
-------------------------(2)逆变
class B : A
void fa(A); void fb(B);
//客户
A a;
B b;
fa(a) or fa(b) //ok
fa 函数体是根据形参来定义的,也就是函数体内只使用A的成员。因此A的对象,B的对象都可以传递进去。
fb(a) or fb(b)
fb函数体内使用的是B类型,假设A的对象可以传递进去,那么当函数体里面使用了B独有的成员,那不就产生错误了么?
所谓的逆变,实际就是fa的对象,但是通过fb访问,既然是通过fb访问,那么客户端就只能传入B类型的参数,因此是可行的。
delegate void f<in T>(T x);
f<B> fb = (A)=>return;
fb(b);
变体有趣的地方是f<A> 和 f<B>完全是独立的类型,没有继承关系,但是却可以“类型转换”,产生类似多态的效果。
三、应用场合