Scala入门到精通——第二十一节 类型参数(三)-协变与逆变
本節主要內容
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 逆變示意圖
- 1
要理解清楚后面的原理,先要理解清楚什么是協變點(covariant position) 和 逆變點(contravariant position)。 
  
 圖2 協變點 
  
 圖3 逆變點 
 我們先假設class Person3[+A]{ def test(x:A){} } 能夠編譯通過,則對于Person3[Any] 和 Person3[String] 這兩個父子類型來說,它們的test方法分別具有下列形式:
- 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 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入门到精通——第二十一节 类型参数(三)-协变与逆变的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: Keras笔记(一)关于Keras模型
- 下一篇: Scala入门到精通——第二十二节 高级
