scala中协变与逆变的理解

转自 : 未知 ,后续发现了补上( [Mr.Snail注] 本文转自一篇文章,那篇文章也是转载的 ,但是没有标注原文链接 - 这里强烈呼吁大家尊重原作者劳动成果,转载文章时,文首就标注原文链接)


范型基础

一句话来说,范型就是定义以类型为参数的类或接口(Scala中为特征)的功能。Java里从JDK5开始就有了范型,想必知道的人应该比较多了,下面就简单举例说明一下。

例如,假设有如下的代码片段。这里java.util.List是范型接口,String就是赋给它的类型参数。

java.util.List< String> strs = new java.util.ArrayList< String>(); 

这样,就可以用如下方法将String类型(或子类型)的对象加入List中了。

strs.add("hoge"); 

如下所示,如将String以外的对象加入List则会发生编译错误。

strs.add(new java.util.Date()); 

这样一来,就可以开发类型安全的通用集(collection)库了。在Java5之前的集库是用Object来实现的。但是向集中加入元素时并没 有进行正确的类型检查,而且从集中取出元素时还要做强制的类型转换,导致旧的集库在类型安全方面有一些问题。进一步来说,光从类型定义看不出该集包含的是 何种元素,所以在可读性方面也有不足。

Scala的范型与Java是非常相似的,基本上可以同样地使用,只是在标记方法上有些区别。以下是同刚才Java代码基本相同的Scala代码。

var strs: java.util.List[String] = new java.util.ArrayList 

Scala中用[..]来代替了Java中的< ..>来表现类型参数表。附带提一下,与Java有一点小的不同,Scala在new ArrayList时不需要指定String类型参数,这是编译器的类型推断起了效用(显示指定也是可以的)。

Scala中定义范型类的方法也基本与Java相同。下面是通过范型用Java定义的不可变单方向列表类。这里在类名Link后声明了用< >括着的类型参数T。这个类型参数T在Link类的定义中可以像一般类型那样使用。

class Link< T> {  
final T head;  
final Link< T> tail;  
Link(T head, Link< T> tail) {  
this.head = head;  
this.tail = tail;  
}  
}  

同样可以用Scala来定义与上述完全相同的范型列表。

class Link[T](val head: T, val tail: Link[T]) 

从此可知,除了一些细微的标识差别,Scala中也可以方便地使用范型。

范型的协变与逆变

光从到此为止的说明来看,可能有人会以为Scala是仅仅把Java中的范型改变了一下标识符号。但是Scala中的范型有几个与Java不同的明显差异,其中之一就是这里提到的协变与逆变。

协变

范型中所谓的协变大致来说是这样的东西。首先假设有类G(或者接口和特征)和类型T1、T2。在T1是T2的子类的情况下如果G< T1>也是G< T2>的子类,那么类G就是协变的。

仅如此说明的话比较难以理解,那就举例说明一下。如下所示,假设有类型为java.util.List< Object>的变量s1和类型为java.util.List< String>的变量s2。

java.util.List< Object> s1 = ...;  
java.util.List< String> s2 = ...; 

String是Object的子类,Java中并不允许将s2赋值给s1,将会产生编译错误。因此,虽然String是Object的子类,但是 java.util.List< String>并不是java.util.List< Object>的子类,所以用Java的范型所定义的类或接口并不是协变的。这并不是由于Java范型的灵活性不好,而是因为协变的范型在保证类型 的安全性上有一些问题。

假定允许s1=s2;。s1是容纳Object类型的元素的,所以如下所示可以加入java.util.Date类型的对象。

s1.add(new java.util.Date()); 

但是由于语句s1=s2;,s1被指向了s2,这样容纳String元素的List变量s2就可以加入java.util.Date对象了。这样好 不容易通过范型来保证的类型安全性(java.util.List< String>里只有String)就被破坏了。正因为有如此问题所以Java的范型不是协变的。

附带提一下,对于Java5之前就存在的数组来说,数组的元素类型A如果是数组元素类型B的子类,那么A的数组类型也是B的数组类型的子类,也就是 说Java中的数组是协变的。这样一来,如下所示即使是违背了类型安全性的数组之间的赋值(没有强制类型转换)代码也能通过编译器检查。

String[] s2 = new String[1];  
Object[] s1 = s2;  
s1[0] = new java.util.Date(); //执行时抛出ArrayStoreException异常 

如上所述,Java中的范型不是协变的是有理由的,但是有些情况下这种限制表现得过于强了。比如,以使用前述的不可变Link类为例。这种情况下, 一旦创建不可变Link的实例之后,与Java的List不同,对于该实例是不能进行写操作(如add)的,这样的话将Link< String>赋值给Link< Object>也就可以认为没有问题了,但是在Java中这是不允许的。

Scala的范型,在没有特定指定的情况下也是和Java一样,是非协变的。例如使用前述的Link类编写如下代码后将会出现编译错误。

val link: Link[Any] = new Link[String]("FOO", null)  
... 

错误提示如下。叙述的错误原因是在应该出现Ling[Any]的地方但是出现了Link[String],而这正是Link不是协变的结果。

fragment of Link.scala):2: error: type mismatch;  
found : this.Link[String]  
required: this.Link[Any]  
val link: Link[Any] = new Link[String]("FOO", null) 

但是,Scala的类或特征的范型定义中,如果在类型参数前面加入+符号,就可以使类或特征变为协变了。下面是在Scala中定义协变类的实验。题材是前述的Link类,在类型参数T前加了一个+符号。

class Link[+T](val head: T, val tail: Link[T]) 

把Link类如此定义之后,前面出现编译错误的代码就可以顺利通过编译了。另外,如果试图定义不能保证类型安全的协变范型将会出现编译错误。例如在 定义非可变的数据结构时,这种限制就会带来一些问题。例如对于前面的Link类,追加一个将作为参数传入的元素放在列表头并返回新列表的方法 prepend。

class Link[+T](val head: T, val tail: Link[T]) {  
def prepend(newHead: T): Link[T] = new Link(newHead, this)  
} 

prepend方法并没有改变原来Link类实例的状态,因该是没有问题的。但是,编译之后会产生如下编译错误。

ink.scala:2: error: covariant type T occurs in contravariant position in type T  
of value newHead  
def prepend(newHead: T): Link[T] = new Link(newHead, this) 

实际上,范型变为协变之后就不能把类型参数不加修改的放在成员方法的参数上(这里是newHead)了。但是,通过将成员方法定义为范型,并按照如下所示描述后就可以避免该问题了(具体原因这里略而不谈)。

class Link[+T](val head: T, val tail: Link[T]) {  
def prepend[U >: T](newHead: U): Link[U] = new Link(newHead, this)  
} 

在Java里也可以定义范型方法,正如范型类型定义,通过用类型参数来参数化方法,从而定义了类型安全的范型方法。例如连载第五回出场的List类的map方法就是范型方法。

verride final def map[B](f : (A) => B) : List[B] 

map方法将以参数形式传入的函数f应用于List的所有元素,并将函数的应用结果组成列表后返回。但是参数函数f的返回结果是什么在定义map方法是不知道的,所以用类型参数B来使map成为范型方法,从而使它可以通用于各种类型了。

范型方法是通过在方法名后直接用[..]来括住类型参数方式来定义的。用[]括住的类型参数在方法中可以作为一般类型来使用。而且在类型参数之后加 上>:或< :符号后,可以将类型参数所表示的类型限制为某一类型子类或父类。例如,[U< :T]的情况下,U必须是T的子类;[U>:T]的情况下,U必须是T的父类。

逆变

另一方面,范型中的逆变是这样的东西。首先假设有类G(或者接口和特征)和类型T1、T2。在T1是T2的子类的情况下如果G< T2>也是G< T1>的子类(注意左右与协变是相反的),那么类G就是逆变的。

与协变一样,下面举例说明一下。首先假设有类型为java.util.List< Object>的变量s1,类型为java.util.List< String>的变量s2。

java.util.List< Object> s1 = ...;  
java.util.List< String> s2 = ...; 

String是Object的子类,由于Java的范型规则不允许表达式s2=s1,所以将会出现编译错误。这里虽然String是Object的 子类,但是java.util.List< Object>并不是java.util.List< String>的子类,所以Java的范型并不是逆变的。如果Java的范型是逆变的话,那同协变时情况一样,将会产生类型安全上的问题。

假设允许表达式s2=s1。由于s2的元素类型是String,所以从列表中取出元素后返回的类型因该是String。因此,如下代码因该是成立的。

String str = s2.get(0); 

但是,s2所指的列表s1的元素类型是Object,所以s1列表中的取出的元素并不仅限于String,这在类型安全性上就有问题了。

对于Scala的范型,如果没有特别指示,与Java一样也不是逆变的。假设有如下含有apply方法的LessTan类(apply方法的逻辑是当a小于b时返回true,否则返回false)。

abstract class LessThan[T] {  
def apply(a: T, b: T): Boolean  
} 

如下使用了LessThan类的方法将会出现编译错误。

val hashCodeLt: LessThan[Any] = new LessThan[Any] {  
def apply(a: Any, b: Any): Boolean = a.hashCode <  b.hashCode  
}  
val strLT: LessThan[String] = hashCodeLt  
... 

编译错误的文本如下。显示的错误原因是在因该出现LessThan[String]的地方出现了LessThan[Any],由此看见LessThan类不是逆变的。

(fragment of Comparator.scala):5: error: type mismatch;  
found : this.LessThan[Any]  
required: this.LessThan[String]  
val strLT: LessThan[String] = hashCodeLt 

但是,在类或特征的定义中,在类型参数之前加上一个-符号,就可定义逆变范型类和特征了。下面尝试一下定义Scala的逆变类。题材是前面的LessThan类,如下所示在LessThan定义的类型参数前加上-符号。

abstract class LessThan[-T] { def apply(a: T, b: T): Boolean } 

将LessThan类如此定义之后,前面错误代码的编译就可以通过了。另外,如果将类型定义为逆变后会发生类型安全性问题,则编译器将报编译错误。

实存(Existantial)类型

前面说过了Java范型没有协变和逆变特性,但是通过使用Java的通配符功能后可以获得与协变与逆变相近的效果。通配符不是标记在类型定义的地 方,而是在类型使用的地方,可以在使用类型处加上G< ? extends T1>或G< ? super T1>。

前者与协变相对应,当T2是T1的子类时,G< T2>是G< ? exnteds T1>的子类。后者与逆变相对应,T1是T2的子类时,G< T2>是G< ? super T1>的子类。因此,以下的代码将能正常编译。

java.util.List< String> s1 = ...;  
java.util.List< ? extends Object> s2 = s1; //对应协变  
java.util.List< Object> s3 = ...;  
java.util.List< ? super String> s4 = s3; //对应逆变  
... 

由于通配符是标记在使用类型的地方,所以每次定义协变或逆变的变量时都要使用它,缺点是比较麻烦。另一方面,即使是没有定义为协变或逆变的范型类型,也可以将其以协变或逆变的方式处理是它的优点。

Scala中也可以通过使用实存类型方法类实现与Java中通配符相同的功能。例如,下述Scala代码可以实现与上述Java代码相同的功能。

//java.util.List[_ < : Any] (省略形式)  
var s1: java.util.List[String] = new java.util.ArrayList  
var s2: java.util.List[T] forSome { type T < : Any } = s1  
//java.util.List[_ >: String] (省略形式)  
var s3: java.util.List[Any] = new java.util.ArrayList  
var s4: java.util.List[T] forSome { type T >: String} = s3  
... 

你可能感兴趣的:(scala中协变与逆变的理解)