本节主要内容

  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<String>并不是的List<Object>子类,也就是说它不是协变的。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)  }
} 
  • 13

可以看到,当不指定类为协变的时候,而是一个普通的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)  }
} 
  • 5

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


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

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){} }
  • 1

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

图2 协变点

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

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

由于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类型的参数。也就是父类能做的事情,子类不一定能做,子类只是部分满足。
为满足里氏替换原则,子类中函数参数的必须是父类中函数参数的超类,这样的话父类能做的子类也能做。因此需要将类中的泛型参数声明为逆变或不变的。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 Personscala> val p1=new Person[AnyRef]()
p1: Person[AnyRef] = Person@8dbd21scala> val p2=new Person[String]()
p2: Person[String] = Person@1bb8caescala> p1.f
res0: AnyRef = nullscala> 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("摇摆少年梦")))
}

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

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

    本节主要内容 Ordering与Ordered特质 上下文界定(Context Bound) 多重界定 类型约束 1. Ordering与Ordered特质 在介绍上下文界定之前,我们对Scala中的 ...

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

    本节主要内容 类型变量界定(Type Variable Bound) 视图界定(View Bound) 上界(Upper Bound)与下界(Lower Bound) 1. 类型变量界定(Type V ...

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

    本节主要内容 XML 字面量 XML内容提取 XML对象序列化及反序列化 XML文件读取与保存 XML模式匹配 1. XML 字面量 XML是一种非常重要的半结构化数据表示方式,目前大量的应用依赖于X ...

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

    本节主要内容 trait构造顺序 trait与类的比较 提前定义与懒加载 trait扩展类 self type 1 trait构造顺序 在前一讲当中我们提到,对于不存在具体实现及字段的trait,它最 ...

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

    本节主要内容 Scala Mavenproject的创建 Scala JDBC方式訪问MySQL Slick简单介绍 Slick数据库编程实战 SQL与Slick相互转换 本课程在多数内容是在官方教程 ...

  6. Scala入门到精通——第二十三节 高级类型 (二)

    本节主要内容 中置类型(Infix Type) 存在类型 函数类型 抽象类型 关于语法糖的问题,在讲解程序语言时,我们常常听到"语法糖"这个术语,在百度百科中,它具有如下定义: 语 ...

  7. Scala入门到精通——第四节 Set、Map、Tuple、队列操作实战

    本节主要内容 mutable.immutable集合 Set操作实战 Map操作实战 Tuple操作实战 队列操作实战 栈操作实战 mutable.immutable集合 以下内容来源于Scala官方 ...

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

    本节主要内容 apply与unapply方法 零变量或变量的模式匹配 提取器与序列模式 scala中的占位符使用总结 1. apply与unapply方法 apply方法我们已经非常熟悉了,它帮助我们 ...

  9. Scala入门到精通——第二十四节 高级类型 (三)

    本节主要内容 Type Specialization Manifest.TypeTag.ClassTag Scala类型系统总结 在Scala中,类(class)与类型(type)是两个不一样的概念. ...

最新文章

  1. Android - Android Studio 解决访问被墙的问题
  2. win8计算机安全模式,Win8如何进入安全模式
  3. 有趣设计工作室创始人段先洲:UI设计师的名利场
  4. 构成子网与构成超网的分析
  5. Java VisualVM 插件地址,安装Visual VM插件,修改下载插件地址使插件可以直接在JVisualVM中进行下载
  6. xxx must either be declared abstract or implement abstract method ‘call(T1, T2)‘ in ‘Function2
  7. MinGW与MSVC编译的区别
  8. JS 浏览器扩展storage
  9. APUE Unix环境高级编程读书笔记
  10. php算法和数据结构
  11. notepad++自动补全括号
  12. wpf 写个简单的控件吧
  13. 最简单vivo机器怎么不root激活XPOSED框架
  14. Android MediaCodec 解码H264/H265码流视频
  15. 程序员的奋斗史(三十二)——人在囧途之应聘篇(二)
  16. ip漂移技术_您的项目是否遭受技术漂移的困扰?
  17. MATLAB中peaks函数的用法
  18. python如何读取数据保存为新格式_运维学python之爬虫中级篇(五)数据存储(无数据库版)...
  19. 禅道配置smtp发信没反应
  20. 互联网大厂的会员“陷阱”

热门文章

  1. 19行代码AC——例题 6-2 铁轨(Rails, UVa 514)——解题报告
  2. 传统公司部署OpenStack(t版)简易介绍(二)——Keystone组件部署
  3. java习题8,Java经典练习题8
  4. access课程均不及格_access 第二章 查询 练习题 -
  5. 某指令引用的内存不能为
  6. vue3.0实现原理
  7. 计算机知识应用,计算机知识应用基础复习大纲
  8. 单臂路由配置实验同一交换机上vlan间ping不通_【干货】什么是单臂路由?如何配置?...
  9. print在python2和python3的区别_Python2和Python3中print的不同点
  10. ubuntu编译内核_鸿蒙源码下载并编译