首页 > 代码库 > scala学习笔记-类型参数中协变(+)、逆变(-)、类型上界(<:)和类型下界(>:)的使用
scala学习笔记-类型参数中协变(+)、逆变(-)、类型上界(<:)和类型下界(>:)的使用
转载自 fineqtbull http://fineqtbull.iteye.com/blog/477994
有位je上的同学来短信向我问起了Scala类型参数中协变、逆变、类型上界和类型下界的使用方法和原理,自己虽然也刚学不久,在主要调查了《Programing in Scala》的19章后,试着在下面做一个总结。如有错误之处还请各位指正。
先说说协变和逆变(实际上还有非变)。协变和逆变主要是用来解决参数化类型的泛化问题。由于参数化类型的参数(参数类型)是可变的,当两个参数化类型的参数是继承关系(可泛化),那被参数化的类型是否也可以泛化呢?Java中这种情况下是不可泛化的,然而Scala提供了三个选择,即协变、逆变和非变。下面说一下三种情况的含义,首先假设有参数化特征Queue,那它可以有如下三种定义。
1)trait Queue[T] {}
这是非变情况。这种情况下,当类型S是类型A的子类型,则Queue[S]不可认为是Queue[A]的子类型或父类型,这种情况是和Java一样的。
2)trait Queue[+T] {}
这是协变情况。这种情况下,当类型S是类型A的子类型,则Queue[S]也可以认为是Queue[A}的子类型,即Queue[S]可以泛化为Queue[A]。也就是被参数化类型的泛化方向与参数类型的方向是一致的,所以称为协变。
3)trait Queue[-T] {}
这是逆变情况。这种情况下,当类型S是类型A的子类型,则Queue[A]反过来可以认为是Queue[S}的子类型。也就是被参数化类型的泛化方向与参数类型的方向是相反的,所以称为逆变。
接着看一个例子。
package fineqtbull.customer//出版物类class Publication(val title: String)//书籍类class Book(title: String) extends Publication(title)//图书库类object Library { //定义图书库内所有的书籍 val books: Set[Book] = Set( new Book("Programming in Scala"), new Book("Walden") ) //打印所有图书内容,使用外部传入的函数来实现 def printBookList(info: Book => AnyRef) { //确认Scala中一个参数的函数实际上是Function1特征的实例 assert(info.isInstanceOf[Function1[_, _]]) //打印 for (book <- books) println(info(book)) } //打印所有图书内容,使用外部传入的GetInfoAction特征的实例来实现 def printBokkListByTrait[P >: Book, R <: AnyRef]( action : GetInfoAction[P, R]) { //打印 for (book <- books) println(action(book)) }}//取得图书内容特征,P类型参数的类型下界是Book,R类型参数的类型上界是AnyReftrait GetInfoAction[P >: Book, R <: AnyRef] { //取得图书内容的文本描述,对应()操作符 def apply(book : P) : R}//单例对象,文件的主程序object Customer extends Application { //定义取得出版物标题的函数 def getTitle(p: Publication): String = p.title //使用函数来打印 Library.printBookList(getTitle) //使用特征GetInfoAction的实例来打印 Library.printBokkListByTrait(new GetInfoAction[Publication, String] { def apply(p: Publication) : String = p.title })}
上例的Library单例对象的printBookList方法使用了函数来取得书籍的内容。在Scala中函数也是对象,上述情况下的函数有一个参数,实际上该参数是如下特征的实例。
trait Function1[-S, +T] { def apply(x: S): T}
printBookList的info参数是Function1类型,而 Function1的-S类型参数是逆变,+T参数是协变。printBookList方法的assert(info.isInstanceOf[Function1[_, _]])语句可以验证这一点。从printBookList方法的定义可以知道,info的S类型参数是Book,T类型参数是AnyRef。然而主函数中使用处则是Library.printBookList(getTitle),getTitle函数中对应的S是Publication,T是String。为什么可以与printBookList原来的定义不一致呢,这就是协变和逆变的威力了。由于-S是逆变,而Publication是Book的父类,所以Publication可以代替(泛化为)Book。由于+T是协变,而String是AnyRef的子类,所以String可以代替(泛化为)AnyRef。如此一来,主程序的语句也就完全正确了。
接下来说说类型的上界和下界,它们的含义如下。
1) U >: T
这是类型下界的定义,也就是U必须是类型T的父类(或本身,自己也可以认为是自己的父类)。
2) S <: T
这是类型上界的定义,也就是S必须是类型T的子类(或本身,自己也可以认为是自己的子类)。
接着使用前面的例子来说明>:和<:的使用方法。printBokkListByTrait方法实现了与printBookList相同的功能,但它是通过传入特征对象来实现的。也就是说,new GetInfoAction[Publication, String] {}和def getTitle(p: Publication): String是等价的,而GetInfoAction定义中使用>:和<:来代替了Function1中+和-。那是由于>:使得Publication可以代替Book,由于<:使得String可以代替AnyRef。
那么为什么Function1中的S是逆变而T是协变呢,那是由apply方法的格式而起的。apply方法的参数类型是S决定了S一定是逆变,而返回类型是T则决定了T是协变,这也是Scala语言的强制规定。
我们再来刨根问底一下,那么为什么Scala要有这种规定呢?这实际上和Liskov代替原理有关,它规定T类型是U类型的子类条件是,在U对象出现的所有地方都可以用T对象来代替。同时对于U和T中相同的方法定义,还必须保证T的参数类型需求的比较少,而T的返回类型提供得比较多。从本文的类子来看,参数类型Publication是Book的父类,所以需求的就比Book少;而返回类型String是AnyRef的子类,所提供的就比AnyRef多。以上就是def getTitle(p: Publication): String可以替代info: Book => AnyRef的原因,也是Scala定义协变和逆变规则的理论基础。
====================================================================
先做一个简化的List定义,List对象由head(第一个元素)和tail(除了第一个元素以外所有后续元素组成的List)组成。Nil是空List对象,由于不论List的泛型类型是什么,空List的含义和行为都没有区别,因此全局只需要存在一个空List对象即Nil。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | trait List[+T] { def isEmpty : Boolean def head : T def tail : List[T] } class Cons[T]( val head : T, val tail : List[T]) extends List[T] { def isEmpty = false } object Nil extends List[Nothing] { def isEmpty : Boolean = true def head : Nothing = throw new NoSuchElementException( "Nil.head" ) def tail : Nothing = throw new NoSuchElementException( "Nil.tail" ) } |
这样就完成了List的定义。我们可以通过下面的方式来定义List对象了。
1 2 | val x : List[String] = Nil val ages : List[Int] = new Cons( 16 , new Cons( 22 , Nil)) |
首先让我们注意一下Nil的定义,Nil这个单例对象所属的类,继承自List[Nothing],为什么可以将List[String]类型的x定义为这个对象?因为泛型类型T是协变的,而Nothing在Scala中是所有其他类的子类。所以List[Nothing]就是List[String]的子类,根据Liskov替换原则,这样的定义是合法的。
下面,我们要像List类增加一个prepend方法,用来生成一个新List,这个新List是在原List头部新增一个元素:
1 2 3 4 | trait List[+T] { // omit other methods def prepend(elem : T) : List[T] = new Cons(elem, this ) } |
但是这样的做法是无法通过编译的。为什么?正如上一篇博客所解释的,协变类型不能作为方法的参数类型。而这样的prepend操作看似是非常符合常理的,那么是Scala的这个规则设定不合理吗?
我们再回想一下Liskov替换原则。如果Bird是Animal的子类,那么List[Bird]就是List[Animal]的子类,那么如果List[Animal]可以prepend一个Animal类型的实例,List[Bird]也可以prepend一个Animal类型的实例,可惜按照上面的定义是不可能的,因此违反了Liskov替换原则,这样的定义是错误的。
为了解决这个问题,我们需要引入泛型类型的下界的概念。
1 2 3 4 | trait List[+T] { // omit other methods def prepend[U > : T](elem : U) : List[U] = new Cons(elem, this ) } |
这个定义的含义是,prepend方法接受一个类型为U的参数,U必须是T或T的父类(“>:”表示泛型类型的下界,而“<:”则相应地表示上界),返回类型则是List[U]。拿Animal和Bird的例子,再假设Chicken是Bird的子类,那么根据这种定义,List[Bird]可以prepend一个Animal的实例,返回List[Animal],而prepend一个Chicken的实例并非不允许,而是U类型不会是Chicken,必须将U类型至少提升至Bird,所以prepend一个Chicken的实例,结果是返回List[Bird]。这样的定义满足Liskov替换原则,所有可以对List[Animal]进行的操作,都可以对List[Bird]进行。
接下来再举一个例子,假设BaldEagle和CrownEagle都是Aeroplane的子类,而BaldEagle和CrownEagle没有关系。那么List[BaldEagle]如果prepend一个CrownEagle的实例会是什么结果呢?类型U不能是CrownEagle,而必须被向上提升直到U是BaldEagle或者BaldEagle的父类,因此U将被提升到Aeroplane,所以结果将是返回List[Aeroplane]。
这样,可以容易地推断出Scala对于泛型上下界的规定。
- 协变类型可以作为泛型类型的下界
- 逆变类型可以作为泛型类型的上界
scala学习笔记-类型参数中协变(+)、逆变(-)、类型上界(<:)和类型下界(>:)的使用