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

作者:摇摆少年梦

视频地址:http://www.xuetuwuyou.com/course/12

本节主要内容

  1. 协变
  2. 逆变
  3. 类型通匹符

1. 协变

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

图1 协变示意图

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

java.util.List<String> s1=new LinkedList<String>();
        java.util.List<Object> s2=new LinkedList<Object>();
        //下面这条语句会报错
        //Type mismatch: cannot convert from
        // List<String> to List<Object>
        s2=s1;

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

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

//定义自己的List类
class List[T](val head: T, val tail: List[T])
object NonVariance {
  def main(args: Array[String]): Unit = {
  //编译报错
  //type mismatch; found :
  //cn.scala.xtwy.covariance.List[String] required:
  //cn.scala.xtwy.covariance.List[Any]
  //Note: String <: Any, but class List
  //is invariant in type T.
  //You may wish to define T as +T instead. (SLS 4.5)
   val list:List[Any]= new List[String]("摇摆少年梦",null)
  }
} 

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

//用+标识泛型T,表示List类具有协变性
class List[+T](val head: T, val tail: List[T])
object NonVariance {
  def main(args: Array[String]): Unit = {
   val list:List[Any]= new List[String]("摇摆少年梦",null)
  }
} 

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

class List[+T](val head: T, val tail: List[T]) {
  //下面的方法编译会出错
  //covariant type T occurs in contravariant position in type T of value newHead
  //编译器提示协变类型T出现在逆变的位置
  //即泛型T定义为协变之后,泛型便不能直接
  //应用于成员方法当中
  def prepend(newHead:T):List[T]=new List(newHead,this)
}
object Covariance {
  def main(args: Array[String]): Unit = {
   val list:List[Any]= new List[String]("摇摆少年梦",null)
  }
} 

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


class List[+T](val head: T, val tail: List[T]) {
  //将函数也用泛型表示
  //因为是协变的,输入的类型必须是T的超类
  def prepend[U>:T](newHead:U):List[U]=new List(newHead,this)

  override def toString()=""+head
}
object Covariance {
  def main(args: Array[String]): Unit = {
   val list:List[Any]= new List[String]("摇摆少年梦",null)
   println(list)
  }
} 

2. 逆变

逆变定义形式如:trait List[+T] {}

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

图2 逆变示意图
//声明逆变
class Person2[-A]{ def test(x:A){} }

//声明协变,但会报错
//covariant type A occurs in contravariant position in type A of value x
class Person3[+A]{ def test(x:A){} }

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

图2 协变点

图3 逆变点

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

//Person3[AnyRef]
def test(x:AnyRef){}

//Person3[String]
def test(x:String){}

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

为满足里氏替换原则,子类中函数参数的必须是父类中函数参数的超类,这样的话父类能做的子类也能做。因此需要将类中的泛型参数声明为逆变或不变的。class Person2[-A]{ def test(x:A){} },我们可以对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){} }合法,可以利用下界进行泛型限定,如:

class Person3[+A]{ def test[R>:A](x:R){} }

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

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

//下面这行代码能够正确运行
class Person4[+A]{
  def test:A=null.asInstanceOf[A]
}
//下面这行代码会编译出错
//contravariant type A occurs
//in covariant position in type ? A of method test
class Person5[-A]{
  def test:A=null.asInstanceOf[A]
}

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

scala> class Person[+A]{def f():A=null.asInstanceOf[A]}
defined class Person

scala> val p1=new Person[AnyRef]()
p1: Person[AnyRef] = [email protected]8dbd21

scala> val p2=new Person[String]()
p2: Person[String] = [email protected]1bb8cae

scala> p1.f
res0: AnyRef = null

scala> p2.f
res1: String = null

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

3. 类型通配符

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

class Person(val name:String){
  override def toString()=name
}

class Student(name:String) extends Person(name)
class Teacher(name:String) extends Person(name)

class Pair[T](val first:T,val second:T){
  override def toString()="first:"+first+"    second: "+second;
}

object TypeWildcard extends App {
  //Pair的类型参数限定为[_<:Person],即输入的类为Person及其子类
  //类型通配符和一般的泛型定义不一样,泛型在类定义时使用,而类型能配符号在使用类时使用
  def makeFriends(p:Pair[_<:Person])={
    println(p.first +" is making friend with "+ p.second)
  }
  makeFriends(new Pair(new Student("john"),new Teacher("摇摆少年梦")))
}

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

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-12 20:47:37

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

Scala入门到精通——第二十节 类型参数(二)

本节主要内容 Ordering与Ordered特质 上下文界定(Context Bound) 多重界定 类型约束 1. Ordering与Ordered特质 在介绍上下文界定之前,我们对scala中的Ordering与Ordered之间的关联与区别进行讲解,先看Ordering.Ordered的类继承层次体系: 通过上面两个图可以看到,Ordering混入了java中的Comparator接口,而Ordered混入了java的Comparable接口,我们知道java中的Comparator是一

Scala入门到精通——第十七节 类型参数(一)

本节主要内容 类型变量界定(Type Variable Bound) 视图界定(View Bound) 上界(Upper Bound)与下界(Lower Bound) 1. 类型变量界定(Type Variable Bound) 类型变量界定是指在泛型的基础上,对泛型的范围进行进一步的界定,从而缩下泛型的具体范围,例如: //下面的类编译通不过 //因为泛型T在编译的时候不能确定其具体类型 //即并不是所有的类中都存在compareTo方法 class TypeVariableBound { d

Scala入门到精通——第二十七节 Scala操纵XML

本节主要内容 XML 字面量 XML内容提取 XML对象序列化及反序列化 XML文件读取与保存 XML模式匹配 1. XML 字面量 XML是一种非常重要的半结构化数据表示方式,目前大量的应用依赖于XML,这些应用或利用XML作为数据交换格式,或利用XML进行文件配置等.像JAVA.C++及其它流行的程序开发语言都是依赖于第三方库来实现XML的操作,例如JAVA经常通过JDOM,DOM4J等XML处理工具进行XML的操纵,但Scala提供了对XML的原生支持,通过scala.xml._包下的类或

Scala入门到精通——第二十节 类型參数(二)

本节主要内容 Ordering与Ordered特质 上下文界定(Context Bound) 多重界定 类型约束 1. Ordering与Ordered特质 在介绍上下文界定之前,我们对scala中的Ordering与Ordered之间的关联与差别进行解说,先看Ordering.Ordered的类继承层次体系: 通过上面两个图能够看到,Ordering混入了java中的Comparator接口.而Ordered混入了java的Comparable接口.我们知道java中的Comparator是一

Scala入门到精通——第十一节 Trait进阶

本节主要内容 trait构造顺序 trait与类的比较 提前定义与懒加载 trait扩展类 self type 1 trait构造顺序 在前一讲当中我们提到,对于不存在具体实现及字段的trait,它最终生成的字节码文件反编译后是等同于java中的接口,而对于存在具体实现及字段的trait,其字节码文件反编译后得到的java中的抽象类,它有着scala语言自己的实现方式.因此,对于trait它也有自己的构造器,trait的构造器由字段的初始化和其它trait体中的语句构成,下面是其代码演示: pa

Scala类型参数中协变(+)、逆变(-)、类型上界(&lt;:)和类型下界(&gt;:)的使用

转自:http://fineqtbull.iteye.com/blog/477994#bc2364938 有位je上的同学来短信向我问起了Scala类型参数中协变.逆变.类型上界和类型下界的使用方法和原理,自己虽然也刚学不久,在主要调查了<Programing in Scala>的19章后,试着在下面做一个总结.如有错误之处还请各位指正. 先说说协变和逆变(实际上还有非变).协变和逆变主要是用来解决参数化类型的泛化问题.由于参数化类型的参数(参数类型)是可变的,当两个参数化类型的参数是继承关系

scala学习笔记-类型参数中协变(+)、逆变(-)、类型上界(&lt;:)和类型下界(&gt;:)的使用

转载自  fineqtbull   http://fineqtbull.iteye.com/blog/477994 有位je上的同学来短信向我问起了Scala类型参数中协变.逆变.类型上界和类型下界的使用方法和原理,自己虽然也刚学不久,在主要调查了<Programing in Scala>的19章后,试着在下面做一个总结.如有错误之处还请各位指正. 先说说协变和逆变(实际上还有非变).协变和逆变主要是用来解决参数化类型的泛化问题.由于参数化类型的参数(参数类型)是可变的,当两个参数化类型的参数

Scala入门到精通——第二十九节 Scala数据库编程

本节主要内容 Scala Mavenproject的创建 Scala JDBC方式訪问MySQL Slick简单介绍 Slick数据库编程实战 SQL与Slick相互转换 本课程在多数内容是在官方教程上改动而来的,官方给的样例是H2数据库上的.经过本人改造,用在MySQL数据库上,官方教程地址:http://slick.typesafe.com/doc/2.1.0/sql-to-slick.html 1. Scala Mavenproject的创建 本节的project项目採用的是Maven P

Scala入门到精通——第二十五节 提取器(Extractor)

作者:摇摆少年梦 视频地址:http://www.xuetuwuyou.com/course/12 本节主要内容 apply与unapply方法 零变量或变量的模式匹配 提取器与序列模式 scala中的占位符使用总结 1. apply与unapply方法 apply方法我们已经非常熟悉了,它帮助我们无需new操作就可以创建对象,而unapply方法则用于析构出对象,在模式匹配中特别提到,如果一个类要能够应用于模式匹配当中,必须将类声明为case class,因为一旦被定义为case class,