泛型和协变逆变

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>完全是独立的类型,没有继承关系,但是却可以“类型转换”,产生类似多态的效果。

三、应用场合

 

 

 

 

 

转载于:https://www.cnblogs.com/Nobel/archive/2012/12/04/2801635.html

### Java 中的逆变及类擦除 #### (Covariance) 是指当子类对象可以被当作父类对象处理时,在中也遵循这一原则。具体来说,如果 `T` 是 `S` 的子类,则 `List<T>` 可以作为 `List<S>` 使用。这主要适用于只读场景。 ```java // 正确:因为 Apple 继承自 Fruit List<? extends Fruit> fruits = Arrays.asList(new Apple()); ``` 这种特性通过通配符 `? extends T` 来实现[^1]。 #### 逆变 (Contravariance) 逆变则相反,它允许使用更广的超类来代替特定的参数。这意味着如果 `A` 是 `B` 的父类,那么 `Consumer<A>` 就能兼容 `Consumer<B>`。此功能特别适合用于写入操作。 ```java // 正确:因为 Object 是所有类的基类 void addItems(List<? super Integer> list) { list.add(1); // 合法的操作 } ``` 这里使用的通配符是 `? super T`。 #### 类擦除 (Type Erasure) Java 中的是在编译期实施的一种机制,运行时不保留具体的类信息。为了保持向后兼容性并简化虚拟机的设计,JVM 并不区分不同类实例;所有的都被视为原始类(raw type)。例如: ```java public class Box<T> { private T content; public void setContent(T t){ this.content=t; } } // 编译后的字节码实际上类似于: class Box { private Object content; public void setContent(Object o){ this.content=o; } } ``` 因此,像下面这样的代码会报错,因为它违反了静态类安全规则[^2]: ```java List<Fruit> fruitBox = new ArrayList<Apple>(); // 错误:无法转换 ``` 上述例子展示了即使 `Apple` 是 `Fruit` 的子类,也不能直接将 `ArrayList<Apple>` 赋给 `List<Fruit>` 类量。这是由于在运行时这些列表都成了普通的 `ArrayList<Object>`,从而失去了类安全性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值