一、前言
刘大胖决定向他的师傅灯笼法师请教什么是协变和逆变。
刘大胖:师傅,最近我在学习泛型接口的时候看到了协变和逆变,翻了很多资料,可还是不能完全弄懂。
灯笼法师:阿胖,你不要被这些概念弄混,编译器可不知道你说的什么协变逆变。这个问题,首先你得弄懂什么叫类型的可变性。
刘大胖:可变性?
二、可变性
灯笼法师:对,可变性是以一种类型安全的方式,将一个对象作为另一对象来引用。虽然是可变,但其实对象的引用地址是不会变的,只是忽悠下编译器。
刘大胖:师傅说的将一个对象作为另一对象来引用?这不就是继承么?
灯笼法师:是的,你可以看下面代码演示(C#):
刘大胖:哦,我理解了,由于MemoryStream继承于Stream,所以MemoryStream的对象可以变为Stream的对象,原来我天天在接触可变性,我竟然不知道。
灯笼法师:是的,这种转变其实遵守了里氏替换原则,爱徒,你可还记得?
刘大胖:当然,为了面试早已烂熟于心。里氏替换原则(LSP):指的是所有引用基类的地方都可以使用其子类的对象。可是师傅,这个和协变逆变有什么关系呢?
三、协变
灯笼法师:协变和逆变只是可变性的分类,主要用于泛型接口和委托中。协变逆变只是类型转换的方向不同。我们先看下接口协变吧,假如有Apple类继承于Fruit,如下:
灯笼法师:然后现在写了一个打印水果名称的方法,如下:
灯笼法师:这时如果你打算打印一些苹果的名称,你会怎么写?
刘大胖:这不是很简单,Apple继承自Fruit,那可以直接使用PrintFruit类了。撸了下,怎么报错了?代码如下:
灯笼法师:大胖,你要理清楚,虽然Apple继承Fruit,但List<Apple>和List<Fruit>却一点关系也没有,如图:
刘大胖:那如果这样,岂不是要为每一种水果都要定义一个PrintFruit方法,我觉得官方不会不知道这个问题吧?
灯笼法师:这种问题,官方当然知道了,所以才有了泛型接口的协变用以支持List<Apple>自动转为List<Fruit>。C#中使用out表示泛型参数的可协变性,List没有out约束,所以不能协变,但它的基类IEnumable却实现了,如图:
灯笼法师:所以只要把PrintFruit的参数类型换成IEnumable就可以了,如图:
刘大胖:那为什么List<T>不能加out以支持协变呢?
灯笼法师:爱徒问的好,List继承于IEnumable,它比IEnumable更宽泛,它支持读和写,但协变只能可读,主要用于约束输出参数。
刘大胖:好吧,我回去再消化下。师傅你再讲一下什么是逆变吧。
四、逆变
灯笼法师:逆变是相反的,即支持List<Fruit>转为List<Apple>,泛型接口上添加in约束输入参数。
刘大胖:有点懞,师傅你还是用代码吧!
灯笼法师:好吧,假如现在我要让苹果列表或桔子列表可以按名称排序,需要一个定义一个水果比较器,此比较器能用于任何种类的水果列表,代码如下:
灯笼法师:现在给苹果和桔子列表按名称排序吧,代码如下:
刘大胖:师傅你别忽悠我,Sort的参数可是要具体类型的比较器的,你看代码:
灯笼法师:大胖,就这是逆变,以使得基类的泛型对象替代子类的泛型对象,主要是因为IComparer<T>中使用了in关键字来约束,代码如下:
五、总结
刘大胖:哦,我有点明白了,协变就是支持泛型子类自动转泛型父类,逆变就是支持泛型父类自动转泛型子类。
灯笼法师:也可以这么理解,但这些转换只是针对编译器,其引用地址并没有改变。
翻外篇1:
协变:String =>Object
逆变:Object => String
翻外篇2:
灯笼法师在刘大胖走后从背后拿出手机,屏幕上显示来不及关闭的知乎APP:
更多精彩文章,可以关注我的公众号:
原文地址:https://www.cnblogs.com/liuzhenbao/p/12638103.html