假设A <: B, 可以简单认为A是B的子类(A <: B的具体定义是:任何B满足的性质,A都满足):
A<:B的前提下,有如下对协变逆变的定义:
C[A] <: C[B] C 是 协变的 covariantC[A] >: C[B] C 是 逆变的 contravariantC[A]和C[B]都不是彼此的子类 C是 非变的 nonvariantC是AnyRef类,即引用类,例如Int是值类,List则是引用类,这是因为List需要定义其元素的类型,比如List[Int]
C[A]代表C的元素是A类对象,C[A]<:C[B]代表C[B]是C[A]基类,因此C[B]可以赋值给C[A]
Scala可以用下面的方式更简单声明容器类的性质
Class C[+A] {..} C 是 协变的 covariantClass C[-A] {…} C 是 逆变的 contravariantClass C[A]{…} C是 非变的 nonvariant例子
Trait List[+T]{ Def perpend(elem:T):List[T] = new Cons(elem, this)}假设基类Inset有两个子类nonEmpty和empty这个定义是错误的,当objectA 是List[nonempty], 语句:objectA.PRepend(empty) 会导致类型匹配错误。
prepend是属于trait List[+T]的方法
(这里List[+T]的用处是,当有对象A为List[NonEmpty], 注意是List对象不是NonEmpty对象,则可以把对象A赋值给一个需要List[Inset]的参数,因为List类是协变的------nonEmpty是Inset子类,List[NonEmpty]便是List[Inset]的子类)
出现错误的情形:
假设此时this是一个List[nonempty]的对象,
则List[T](List[+T]赋予List的性质在这里不用考虑了),编译器会识别T是nonEmpty类,所以如果this.prepend(empty)会导致类型错误,因为empty和nonEmpty都是InSet的子类,但是empty不是nonEmpty的子类,不能赋值给需要nonEmpty的参数位置(elem:T)。
正确的定义方式:
Def perpend[U>:T](elem: U) :List[U] =new Cons(elem, this)
U>:T 表示:U是T的基类,T是U的子类。因此原先List[nonEmpty]的对象,T是nonEmpty,但是U是T的基类,所以U是Inset。传入prepend方法的元素要求是Inset类型,empty是Inset子类当然可以传入,得到的new Cons(empty, this)是List[Inset],毫无问题
应用
1 ).选择支持协变的容器类:
1. Inset class 中有NoEmpty 和 empty两个子类。
2. Array不是协变,即定义abstractclass Array[T] extends Seq[T]。
3. 如果Array换成List, 因为定义abstractclass List[+T] extends Seq[T],List是协变,则NonEmpty是Inset的子类,List[NonEmpty]是List[Inset]的子类,第二行的a可以赋值给b。类似于c++的多态,子类指针和子类引用是可以分别赋值给基类指针和基类引用
2). 定义新的函数对象,例如
object addOne extends Function1[Int,Int]{ def apply(m:Int):Int= m + 1}利用函数的协变:If A2 <: A1 并且 B1<:B2 则有:
A1=> B1 <: A2 => B2
因此scala中的Function1特性的定义如下:
Package scala
Trait Function1[-T, +U] { def apply(x:T) : U
}
假设有两个Function1:
scala> val f1: Int => String = x=> s"Int($x)"
f1: Int => String = <function1>
scala> val f2: Any => String = x=> s"Any($x)"
f2: Any => String = <function1>
凡是f1可以使用的地方,f2都是可以用的(考虑基类子类的关系:基类可以使用的场景,子类都可以使用);但反过来不行。
所以类型Function1[Any, String]应该是Function1[Int, String]的子类, 是Trait Function1[-T, +U]
的定义赋予了Function1类更丰富的继承关系。
逆变的传入类型,允许子类在重写基类的函数时,传入参数的类型比基类原函数定义的传入参数类型更广泛。
而协变的返回类型,允许了子类在重写基类的函数时,可以返回比基类原函数定义的返回类型更加具体的类型。
新闻热点
疑难解答