Scala的型变
在scala中Any
是任何基础数据类型的父类,但是,在scala中是不允许ArrayList[Int]的引用赋值给一个指向ArrayList[Any]的引用。像下面这样:
object TestMain extends App {
var arrInt:Array[Int] = Array(1, 2, 3, 4)
var arrAny:Array[Any] = _
arrAny = arrInt // 这是不被允许的,编译错误
}
通常情况下,一个派生类型的集合不应该赋值给一个基类型的集合。然而,在有些情况下,我们需要放宽这一规则。这种情况下,我们可以要求scala允许在其他庆康下无效的转换。
协变和逆变
如上面的例子,这种情况下编译是不被允许的,可以强行编译一下,看看具体的编译错误报告:
Error:(15, 13) type mismatch;
found : Array[Int]
required: Array[Any]
Note: Int <: Any, but class Array is invariant in type T.
You may wish to investigate a wildcard type such as `_ <: Any`. (SLS 3.2.10)
arrAny = arrInt// 这行是不被允许的,编译错误
相比java而言,scala是友好的,java对此在编译阶段是没有限制的,但是在错误会发生在运行阶段,也就是说,在java中,当把一个苹果new Apple()
试图放进一个盛香蕉的篮子里Banana[]
中的时候(当然,二者都是fruit的子类),编译阶段是不会报错的,但是在如果你想要吧一个命名为Apple
的篮子的对象赋值给命名为Object
的对象的话,同样,Java语言也是不允许这样做的:
public class TestJavaMain {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
ArrayList<Object> list2 = list; //编译错误
}
}
但是,在java中个,这个问题是很好解决的:ArrayList list2 = list;
- 协变:当在希望接收一个基类实例的集合的地方,能够使用一个子类实例的集合的能力叫做协变(convariance)
- 逆变:当在希望接收一个子类实例的集合的地方,能够使用一个超类实例的集合的能力叫做逆变(contravariance)
默认情况下,scala是二者都不允许的,即不变
协变
需求:
- 定义两个类,分别是Dog类,Pet类
- Dog类继承Pet类
- 主类中定义一个方法
workWithPets
用来接收一个Pet数组的参数 - 要求
workWithPets
该方法能够接受一个Dog
数组的参数,即实现协变
class Pet(val name: String) {
override def toString: String = name
}
class Dog(override val name: String) extends Pet(name)
object TestMain {
def main(args: Array[String]): Unit = {
val dogs = Array(
new Dog("Rover"),
new Dog("Comet")
)
workWithPets(dogs) // 编译错误
workWithPets_t(dogs) // 正常运行
}
// 接受pet数组的方法
def workWithPets(Pet: Array[Pet]) = {}
// 改进后
def workWithPets_t[T <: Pet](pets: Array[T]) = {
println("Playing with pets:" + pets.mkString(", "))
}
}
这里使用T <: Pet
符号来定义协变,这里T
代表的是Pet
的上界,通过指定上界,告诉scala,T
必须至少是一个Pet
的派生数组。
在通俗一点讲,协变即是:本可以接受一个父类型的参数的方法,通过协变可以使其接受一个子类的参数
逆变
现在,假设我们需要见个宠物的A
集合复制到B
集合中,那么,可以编写一个名为copy()的方法,它接受两个类型为Array[Pet]的参数。也就是说,这种场景下,B
作为接受数组,它的元素的类型是源数组A
中元素类型的超类,也就是这里我们需要一个下界:
def copy[A, B >: A](fromPet: Array[A], toPet: Array[B]) = {
println("A:" + fromPet.mkString(", ") + " B:" + toPet.mkString(", "))
}
这里限定了目标数组的参数化类型(B),将其限定为了源数组的参数话类型(A)的一个超类型。话句话说,A设定了类型B的下界--B它可以是类型A,也可以是A的超类型
集合的型变
如果我们需要在集合中使用派生类型,也可以通过参数化类型来完成这一操作。
例如:假设派生类型的集合可以被看做是其基类型的集合。你可以通过将参数化类型标记为+T
来完成次操作
class MyList[+T] {
}
val list1 = new MyList[Int]
var list2: MyList[Any] = new MyList[Any]
list2 = list1
这里 +T
告诉scala允许协变,也就是说,在类型检查期间,他要求scala接受一个类型或者该类型的派生类型。因此可以将一个MyList[Int]
的实例赋值给一个MyList[Any]
的引用,需要记住的是,这不能是Array[Int].然而,这可以是scala库中实现的List。
同样,可以使用参数化类型-T
而不是T
,我们可以要求scala为自己的类型提供逆变支持。