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)
}
}
可以看到,當不指定類為協變的時候,而是一個普通的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相關技術資訊