1. 程式人生 > >Scala入門到精通——第二十一節 型別引數(三)-協變與逆變

Scala入門到精通——第二十一節 型別引數(三)-協變與逆變

本節主要內容

  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) } }

可以看到,當不指定類為協變的時候,而是一個普通的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[Any] 和 Person3[String] 這兩個父子型別來說,它們的test方法分別具有下列形式:

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

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

由於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 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相關技術資訊
這裡寫圖片描述