概述
写在前头,网上关于协变逆变的文章写写得真是让人一头雾水,微软官方文档更是重量级,仿佛不想让人懂。
按照官方解释:
- 协变:能够使用比原始指定的派生类型的派生程度更大(更具体)的类型;
- 逆变:能够使用比原始指定的派生类型的派生程度更新(更抽象)的类型;
为什么需要协变与逆变
假设有一个这样的场景:
Speak<Chinese> speak1 = new Speak<Chinese>();
Speak<Human> speak2 = speak1;
一般情况下,这样的直接指向是不可以的,编译器会报错;
这个时候就需要用到协变和逆变了;
认识协变与逆变
首先,使用协变与逆变有一个前提,就是只能用在接口或者委托上;
协变
协变需要在接口的泛型参数上加上out:
public interface ISpeak<out T>
{
public T SpeakLanguage();
}
这里的out可以理解为出参,就是T在这个接口里只能作为返回值使用,或者作为只读属性的类型使用,不能作为方法的参数使用;
这个时候再执行:
Speak<Human> speak2 = speak1;
编译器就不会报错了;
因此可以总结协变的解释:如果存在一个泛型接口 Interface<T>,它的泛型参数子类类型 Interface<Chinese> 可以安全地转换为泛型父类类型 Interface<Human>,这个过程就称为协变;
逆变
协变需要在接口的泛型参数上加上in:
public interface ISpeak<in T>
{
public void SpeakLanguage(T t);
}
逆变中T只能作为方法的入参的类型使用,不能作为返回值使用;
当使用逆变时,执行:
Speak<Human> speak1 = new Speak<Human>();
Speak<Chinese> speak2 = speak1;
编译器也不会报错;
所以逆变的解释为:如果存在一个泛型接口 Interface,它的泛型参数父类类型 Interface<Human> 可以安全地转换为泛型子类类型 Interface<Chinese>,这个过程就称为逆变;
要注意的是,并不是说逆变就是把父类的对象引用安全地指向子类的对象引用了,协变与逆变是不能抛开泛型谈的;假如我们不以泛型讨论这个问题,众所周知父类的对象引用是不能安全地指向子类对象引用的,相反,子类的对象引用是可以安全地指向父类的,也就是子类转换为父类是安全的;
深入了解协变与逆变
协变
假设有这样的一个场景:
public interface ISport<T>
{
void Method1(T param);
T Method2();
}
public class Sport<T> : ISport<T>
{
public void Method1(T param)
{
}
public T Method2() => default(T);
}
当我们尝试运行这样的代码:
ISport<Human> sport = new Sport<Chinese>();
var temp = sport.Method2();
sport.Method1();
对于 sport,它在调用 sport.Method2() 方法的时候,要求返回的是一个 Human 类型的对象,但在这个代码中,Method2() 方法是在 Sport<T> 中实现的,这里返回的是一个 Chinese类型;对于这种情况,是安全的,没有问题;
但是同样的道理。对于Method1() 来说就不安全了,当调用Method1() 的时候,按实现应该传入一个 Chinese 类型的对象,但因为是使用sport调用的,所以只能传入一个Human类型的对象,此时就不安全了,这就是为什么要用协变限制T只能为返回值;
逆变
和协变差不多,懒得写了orz;