天天看点

Scala入门到精通——第二十一节 类型参数(三)-协变与逆变

作者:摇摆少年梦

协变

逆变

类型通匹符

协变定义形式如:trait list[+t] {} 。当类型s是类型a的子类型时,则list[s]也可以认为是list[a}的子类型,即list[s]可以泛化为list[a]。也就是被参数化类型的泛化方向与参数类型的方向是一致的,所以称为协变(covariance)。

Scala入门到精通——第二十一节 类型参数(三)-协变与逆变

图1 协变示意图

为方便大家理解,我们先分析java语言中为什么不存在协变及下一节要讲的逆变。下面的java代码证明了java中不存在协变:

虽然在类层次结构上看,string是object类的子类,但<code>list&lt;string&gt;</code>并不是的<code>list&lt;object&gt;</code>子类,也就是说它不是协变的。java的灵活性就这么差吗?其实java不提供协变和逆变这种特性是有其道理的,这是因为协变和逆变会破坏类型安全。假设java中上面的代码是合法的,我们此时完全可以s2.add(new person(“摇摆少年梦”)往集合中添加person对象,但此时我们知道, s2已经指向了s1,而s1里面的元素类型是string类型,这时其类型安全就被破坏了,从这个角度来看,java不提供协变和逆变是有其合理性的。

scala语言相比java语言提供了更多的灵活性,当不指定协变与逆变时,它和java是一样的,例如:

可以看到,当不指定类为协变的时候,而是一个普通的scala类,此时它跟java一样是具有类型安全的,称这种类是非变的(nonvariance)。scala的灵活性在于它提供了协变与逆变语言特点供你选择。上述的代码要使其合法,可以定义list类是协变的,泛型参数前面用+符号表示,此时list就是协变的,即如果t是s的子类型,那list[t]也是list[s]的子类型。代码如下:

上述代码将list[+t]满足协变要求,但往list类中添加方法时会遇到问题,代码如下:

那如果定义其成员方法呢?必须将成员方法也定义为泛型,代码如下:

逆变定义形式如:trait list[-t] {}

当类型s是类型a的子类型,则queue[a]反过来可以认为是queue[s}的子类型。也就是被参数化类型的泛化方向与参数类型的方向是相反的,所以称为逆变(contravariance)。 下面的代码给出了逆变与协变在定义成员函数时的区别:

Scala入门到精通——第二十一节 类型参数(三)-协变与逆变

图2 逆变示意图

要理解清楚后面的原理,先要理解清楚什么是协变点(covariant position) 和 逆变点(contravariant position)。

Scala入门到精通——第二十一节 类型参数(三)-协变与逆变

图2 协变点

Scala入门到精通——第二十一节 类型参数(三)-协变与逆变

图3 逆变点

我们先假设<code>class person3[+a]{ def test(x:a){} }</code> 能够编译通过,则对于person3[any] 和 person3[string] 这两个父子类型来说,它们的test方法分别具有下列形式:

由于anyref是string类型的父类,由于person3中的类型参数a是协变的,也即person3[any]是person3[string]的父类,因此如果定义了val pany=new person3[anyref]、val pstring=new person3[string],调用pany.test(123)是合法的,但如果将pany=pstring进行重新赋值(这是合法的,因为父类可以指向子类,也称里氏替换原则),此时再调用pany.test(123)时候,这是非法的,因为子类型不接受非string类型的参数。也就是父类能做的事情,子类不一定能做,子类只是部分满足。

为满足里氏替换原则,子类中函数参数的必须是父类中函数参数的超类,这样的话父类能做的子类也能做。因此需要将类中的泛型参数声明为逆变或不变的。<code>class person2[-a]{ def test(x:a){} }</code>,我们可以对person2进行分析,同样声明两个变量:val panyref=new person2[anyref]、val pstring=new person2[string],由于是逆变的,所以person2[string]是person2[anyref]的超类,panyref可以赋值给pstring,从而pstring可以调用范围更广泛的函数参数(比如未赋值之前,pstring.test(“123”)函数参数只能为string类型,则panyref赋值给pstring之后,它可以调用test(x:anyref)函数,使函数接受更广泛的参数类型。方法参数的位置称为做逆变点(contravariant position),这是class person3[+a]{ def test(x:a){} }会报错的原因。为使class person3[+a]{ def test(x:a){} }合法,可以利用下界进行泛型限定,如:

将参数范围扩大,从而能够接受更广泛的参数类型。

通过前述的描述,我们弄明白了什么是逆变点,现在我们来看一下什么是协变点,先看下面的代码:

这里我们同样可以通过里氏替换原则来进行说明

可以看到,定义为协变时父类的处理范围更广泛,而子类的处理范围相对较小;如果定义协变的话,正好与此相反。

类型通配符是指在使用时不具体指定它属于某个类,而是只知道其大致的类型范围,通过”_ &lt;:” 达到类型通配的目的,如下面的代码

添加公众微信号,可以了解更多最新spark、scala相关技术资讯

Scala入门到精通——第二十一节 类型参数(三)-协变与逆变