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是二者都不允许的,即不变

协变

需求:

  1. 定义两个类,分别是Dog类,Pet类
  2. Dog类继承Pet类
  3. 主类中定义一个方法workWithPets用来接收一个Pet数组的参数
  4. 要求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为自己的类型提供逆变支持。

posted @ 2021-07-07 22:04  郭小白  阅读(193)  评论(0编辑  收藏  举报