逆变和协变在存在于强类型语言中,虽然很少提及,但是里面蕴含了面向对象的世界观。感谢和我一起讨论这个问题的人。
这里用了C#、Scala的语法作为示例,其实逆变和协变的概念跟语言本身关系不大,事实也是如此。
一、定义
逆变的参数可以由指定的类型的子类型代替,协变的参数可以由指定类型的父类型代替。
Scala中的逆变声明:Function[-A,+B] ;其中泛型-A为逆变类型,在实例化时,可以使用A类型或者A类的子类型。
二、协变与逆变的用途不同
1.语义
Scala中,函数的原型之一包含Function1[-A,+B],表示一个A类型的输入,B类型的输出(返回),替换的语法为A=>B
这个函数的定义就好像说,我需要类A帮我做一些事情,处理完之后给你一个B。而A可以完成的工作,其子类也应该能够完成,这正是里氏替换原则——父类出现的地方都可以用子类代替。逆变强调功能——“能做什么”。
顺便看下协变,输出为协变,表示我会给你你个B对象,如果B是肉,我当然可以说给了你食物,而食物是肉的父类,恰好是协变。如果使用逆变则说不通。协变强调类型——“是什么”。
2.刀和肉的例子
类型:食品<-肉<-牛肉。武器<-刀<-牛肉刀。(<-表示继承关系:父类<-子类)
情景:继续拿Function1做示例,如果Function1需要一把刀,会生产出肉。大致为Function(A):B普通刀(刀类)会生产出普通肉(肉类),牛肉刀会生产出牛肉。
问题:A的类型?B的类型?如何确定
A的类型可以为刀和牛肉刀,因为牛肉刀也是刀。甚至说刀的子类都能够满足条件——都有刀的功能。从继承来讲刀的子类都是刀。
所以A的类型应该为逆变——-刀(刀和子类)
因为做出的是肉,所以B类型肯定包含肉,但不确定是牛肉。所以我们可以设定返回为肉类型。
对于这个情景,我们对FunctionX的最终定义为:FunctionX(-刀):肉
没有协变?
我们没有看到协变,实际上在C#和Scala中,我们设定一个食品类型 来接收FunctionX的返回值也不会报错。因为所有的返回类型在语言中都被声明为协变了,也就是说实际的定义是FunctionX(-刀):+肉。这么做的原因是:如果我返回了一个肉,那么这个肉一定是食品,我总能用返回类型的父类型代替返回的对象。这种行为也是多态一方面的体现——在运行时改变了引用的实际类型。我认为,这是编译层面上的协变。
三、C#中的例子
ICompareable<in T>强调“可比较”这一功能,是逆变。
IEnumerable<out T>强调的是“可数的”类型,是协变。拿List<T>说明,List<肉>表示“我放了肉在列表里面”,也可以说"我放了食物在列表里面",即可以使用List<食品>代替。但是不能说“我放了牛肉在列表里面”,所以用List<牛肉>代替是不对的。