环境:
- window 10
- .netcore 3.1
- vs2019 16.5.1
一、为什么要有协变?
首先看下面的代码:
还有下面的:
其实上面报错的是同一个问题,就是你无法用List<Fruit>指向List<Apple>!
我们的疑问在于,明明是一个盛放苹果的箱子,我们说它可以盛放水果怎么了???
下面我来说一下原因:
- 首先,不能根据这个类的用途去判断,因为你无法保证List这个类一定是集合(List当然是集合,但如果是
Person<T>呢,它是做什么的?只是盛放东西吗?)。 - 其次,
Apple继承自Fruit没错,但List<Apple>和List<Fruit>压根就没有继承的说法,它们是不同的类型(泛型参数类型不同也是不同的类型):Console.WriteLine(typeof(ITest<Apple>) == typeof(ITest<Fruit>));输出为:false
所以,我们用List<Fruit>去表示List<Apple>引发报错很正常!!!
但是,从我们程序员角度来说,这样肯定不方便,那么有没有解决办法呢?
答案:有,它就是协变!
二、什么是协变?
首先,明确一下目的:我们想让List<Fruit> list = new List<Apple>();这类代码成立!(这行代码肯定不成立,我说的是这类代码)
想要达到我们的目的,肯定是要有规则的:
- 必须使用接口进行指向,不能使用类:
比如说:我们只能这么写
IList<Fruit> list = new List<Apple>();(虽然这样写也报错),不能够这么写List<Fruit> list = new List<Apple>();
为什么不能使用类?因为类里面牵扯到的东西比较多,而接口里面比较简单,所以只在接口上做规则就行了(这一点是猜的)。 - 这个接口的泛型参数只能用来做接口内方法的返回值,不能用作接口内方法的参数(在泛型参数前加
out关键字实现):这里从两方面说:
1.允许这个泛型参数做返回值:Fruit可以作为方法的返回值,而实际上人家返回的就是Fruit的子类Apple,所以一点问题没有。
2.禁止这个泛型参数做方法的入参:假如泛型参数做方法的入参,调用者看到这个方法接收的参数可以是Fruit,那么调用者就有可能传一个
orange(橙子,也集成了Fruit)进去,而人家实际上是一个IList<Apple>,这样肯定说不通!所以泛型参数禁止做方法的入参!
上面说了规则,那么下面来一个实例:
可以看到,我们按照规则在ITest的泛型参数T上加了out后,整个程序腰部酸了、腿不疼了。
事实上,微软已经在集合上的定义上已经考虑到了这一点,看一下IEnumerable的定义:
所以,我们向下面这样写也没有错:
讲到这里,我们可以说一下什么是协变了:
假如有两个类:A和AA,其中AA集成子A,如果此时有一个泛型接口IC<out T>,那么可以认为IC<A>能指向IC<AA>。
三、什么是逆变?
如果你理解了上面说的什么是协变,那么我说:逆变和协变是相对的,你一定也能理解。
具体来说:
逆变的目的是:让List<Apple> test = new List<Fruit>();这类代码成立!(这行代码肯定报错,我说的是这类代码)
你一定认为这肯定疯了,“说一个盛放水果的箱子盛放的是苹果”肯定不对。
上面那个代码肯定不成立,但是我们看下面的实例:
上图中的实例是不是颠覆了你的认知?
好吧,这就是协变:一个可以让你用ITest<Apple>去指向Test<Fruit>()的存在!
这里还在再说一下协变的规则:
- 必须使用接口进行指向,不能使用类:
这一点和协变是一样的。
- 这个接口的泛型参数只能用来做接口内方法的入参,不能用作接口内方法的返回值(在泛型参数前加
in关键字实现):这里从两方面说:
1.允许这个泛型参数做入参:Apple作为接口内方法的入参,那么调用者实际传入的是Fruit,一点问题没有。
2.禁止这个泛型参数做方法的返回值:假如泛型参数Apple做方法的返回值,那么调用者可能就使用Apple类型去接收这个返回值,然而人家可能返回的只是一个Fruit,这肯定不行!所以泛型参数禁止做方法的返回值!
四、委托内的协变和逆变
上面说协变和逆变的时候只在接口内做了说明,这里说一下委托内的逆变和协变!
未完待续。。。