c#: 协变和逆变深度解析

环境:

  • 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(List<Apple>) == typeof(List<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.允许这个泛型参数做返回值:比如定义接口ITest<out T>,允许T作为接口内方法MethodA的返回值(T MethodA();)。在使用的时候,你用ITest<Fruit>指向ITest<Apple>,那么当调用ITest<Fruit>的方法MethodA的时候你得到的返回类型声明是Fruit,实际上你得到的返回类型是Apple,所以一点问题没有。
    2.禁止这个泛型参数做方法的入参:比如定义接口ITest<out T>,允许T作为接口内方法MethodA的入参(void MethodA(T t);)。在使用的时候,你用ITest<Fruit>指向ITest<Apple>,那么当调用ITest<Fruit>的方法MethodA的时候你看到这个方法要求传入一个Fruit,所以你可能传一个
    orange(橙子,也继承了Fruit)进去,但人家实际上是ITest<Apple>,要求传入的是Apple,这样肯定说不通!所以泛型参数禁止做方法的入参!

上面说了规则,那么下面来一个实例:
在这里插入图片描述
可以看到,我们按照规则在ITest的泛型参数T上加了out后,整个程序腰不酸了、腿不疼了。
事实上,微软在集合的定义上已经考虑到了这一点,看一下IEnumerable的定义:
在这里插入图片描述
所以,我们像下面这样写也没有错:
在这里插入图片描述
讲到这里,我们可以说一下什么是协变了:
假如有两个类:AAA,其中AA继承自A,如果此时有一个泛型接口IC<out T>,那么可以认为IC<A>能指向IC<AA>,即:IC<AA>IC<A>的关系看着像AAA的关系一样(只是看着像,并且能单方向转换,但不是继承!!!)。

三、什么是逆变?

逆变和协变是相对的,具体来说:
逆变的目的是:让List<Apple> test = new List<Fruit>();这类代码成立!(这行代码肯定报错,我说的是这类代码)
你一定认为这疯了,“说一个盛放水果的箱子盛放的是苹果”肯定不对。
但是我们看下面的实例:
在这里插入图片描述
上图中的代码是不是颠覆了你的认知?
好吧,这就是逆变:一个可以让你用ITest<Apple>去指向Test<Fruit>()的存在!
这里还是再说一下逆变的规则:

  • 必须使用接口进行指向,不能使用类:

    这一点和协变是一样的。

  • 这个接口的泛型参数只能用来做接口内方法的入参,不能用作接口内方法的返回值(在泛型参数前加in关键字实现):

    这里从两方面说:
    1.允许这个泛型参数做方法的入参:比如定义接口ITest<in T>,允许T作为接口内方法MethodA的入参(void SetValue(T t);)。在使用的时候,你用ITest<Apple>指向ITest<Fruit>,那么当你调用ITest<Apple>的方法MethodA的时候你看到这个方法要求传入一个Apple,实际上人家是ITest<Fruit>,人家要求传入的是Fruit,所以这里一点问题没有。
    2.禁止这个泛型参数做方法的返回值:比如定义接口ITest<in T>,允许T作为接口内方法MethodA的返回值(T GetValue();)。在使用的时候,你用ITest<Apple>指向ITest<Fruit>,那么当调用ITest<Apple>的方法MethodA的时候你得到的返回类型声明是Apple,但实际上人家是ITest<Fruit>,所以返回的是一个orange(橙子,也继承了Fruit)也说不定,所以你用Apple去接收这个返回值肯定不行的,所以泛型参数禁止做方法的返回值!

四、委托内的协变和逆变

委托中的泛型参数是天然就可以支持协变或逆变中的一种的!

对这句话的理解如下:

  • 如果你这么定义委托:public delegate T GetValue<T>();,那么它天然支持协变(因为T只用来声明返回值),如下代码:
    在这里插入图片描述
  • 如果你这么定义委托:public delegate void SetValue<T>(T t);,那么它天然支持逆变(因为T只用来做入参),如下代码:
    在这里插入图片描述
  • 如果你这么定义委托,它既不支持协变,也不支持逆变:public delegate T Deal<T>(T t);(因为T即用来做入参也用来做返回值),如下代码:
    在这里插入图片描述

其实,在委托中为了更好的表示泛型参数是支持协变还是逆变,最好是定义的时候就用outin参数进行声明,比如:
public delegate T GetValue<out T>();//支持协变
public delegate void SetValue<in T>(T t);//支持逆变

微软在Func、Action系列委托中已经为我们做了示范:
在这里插入图片描述在这里插入图片描述

  • 2
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jackletter

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值