Scala教程之:深刻理解協變和逆變

在以前的文章中咱們簡單的介紹過scala中的協變和逆變,咱們使用+ 來表示協變類型;使用-表示逆變類型;非轉化類型不須要添加標記。安全

假如咱們定義一個class C[+A] {} ,這裏A的類型參數是協變的,這就意味着在方法須要參數是C[AnyRef]的時候,咱們能夠是用C[String]來代替。app

一樣的道理若是咱們定義一個class C[-A] {}, 這裏A的類型是逆變的,這就意味着在方法須要參數是C[String]的時候,咱們能夠用C[AnyRef]來代替。ide

注意:變異標記只有在類型聲明中的類型參數裏纔有意義,對參數化的方法沒有意義,由於該標記影響的是子類繼承行爲,而方法沒有子類。例如List.map 方法的簡化簽名:
sealed abstract class List[+A] ... { // 忽略了混入的trait
...
def map[B](f: A => B): List[B] = {...}
...
}
這裏方法map的類型參數B是不能使用變異標記的,若是你修改其變異標記,則會返回編譯錯誤。

函數的參數和返回值

如今咱們討論scala中函數參數的一個很是重要的結論:函數的參數必須是逆變的,而返回值必須是協變的函數

爲何呢?this

接下來咱們考慮scala內置的帶一個參數的函數類型Function1,其簡化的定義以下:scala

trait Function1[-T1, +R] extends AnyRef { self =>
  /** Apply the body of this function to the argument.
   *  @return   the result of function application.
   */
  def apply(v1: T1): R

...

  override def toString() = "<function1>"
}

咱們知道相似 A=>B 的形式在scala中是能夠自動被轉換爲Function1的形式。code

scala> var f: Int=>Int = i=>i+1
f: Int => Int = <function1>

實際上其會被轉換成爲以下的形式:繼承

val f: Int => Int = new Function1[Int,Int] {
def apply(i: Int): Int = i + 1
}

假如咱們定義了三個class 以下:教程

class CSuper { def msuper() = println("CSuper") } 
class C extends CSuper { def m() = println("C") }
class CSub extends C { def msub() = println("CSub") }

咱們能夠定義以下幾個f:get

var f: C => C = (c: C) => new C // ➋
f = (c: CSuper) => new CSub // ➌
f = (c: CSuper) => new C // ➍
f = (c: C) => new CSub // ➎
f = (c: CSub) => new CSuper // ➏ 編譯錯誤!

根據Function1[-T1, +R]的定義,2-5能夠經過編譯,而6會編譯失敗。

怎麼理解6呢? 這裏咱們要區分兩個概念,函數的定義類型和函數的運行類型。

這裏f的定義類型是 C=>C。 當f = (c: CSub) => new CSuper時,它的實際apply方法就是:

def apply(i: CSub): CSuper = new CSuper

CSub=>CSuper就是f的運行類型。

在apply中能夠能調用到CSub特有的方法,例如:msub(),而返回的CSuper又缺乏了C中的方法 m()。

若是用戶在調用該f的時候,仍是按照定義的類型傳入C,而且期待返回的值是C時候,就會發生錯誤。 由於實際的類型是按照傳入CSub和返回CSuper來定義的。

若是實際的函數類型爲(x:CSuper)=> Csub,該函數不只能夠接受任何C 類值做爲參數,也能夠處理C 的父類型的實例,或其父類型的其餘子類型的實例(若是存在的話)。因此,因爲只傳入C 的實例,咱們永遠不會傳入超出f 容許範圍外的參數。從某種意義上說,f 比咱們須要的更加「寬容」。

一樣,當它只返回Csub 時,這也是安全的。由於調用方能夠處理C 的實例,因此也必定能夠處理CSub 的實例。在這個意義上說,f 比咱們須要的更加「嚴格」。

若是函數的參數使用了協變,返回值使用了逆變則會編譯失敗:

scala> trait MyFunction2[+T1, +T2, -R] {
| def apply(v1:T1, v2:T2): R = ???
| }
<console>:37: error: contravariant type R occurs in covariant position
in type (v1: T1, v2: T2)R of method apply
def apply(v1:T1, v2:T2): R = ???
^
<console>:37: error: covariant type T1 occurs in contravariant position
in type T1 of value v1
def apply(v1:T1, v2:T2): R = ???
^
<console>:37: error: covariant type T2 occurs in contravariant position
in type T2 of value v2
def apply(v1:T1, v2:T2): R = ???
^

可變類型的變異

上面咱們講的狀況下,class的參數化類型是不可變的,若是class的參數類型是可變的話,會是什麼樣的狀況呢?

scala> class ContainerPlus[+A](var value: A)
<console>:34: error: covariant type A occurs in contravariant position
in type A of value value_=
class ContainerPlus[+A](var value: A)
^
scala> class ContainerMinus[-A](var value: A)
<console>:34: error: contravariant type A occurs in covariant position
in type => A of method value
class ContainerMinus[-A](var value: A)

經過上面的例子,咱們也能夠獲得一個結論,可變參數化類型是不能變異的

假如可變參數是協變的ContainerPlus[+A],那麼對於:

val cp: ContainerPlus[C]=new ContainerPlus(new CSub)

定義的類型是C,可是運行時類型是CSub,若是須要對類型變量從新賦值時就會遇到將C賦值給CSub的狀況,會出現編譯錯誤。

若是可變參數是逆變的ContainerPlus[-A],那麼對於:

val cm: ContainerMinus[C] = new ContainerMinus(new CSuper)

定義的類型是C,可是運行時類型是CSuper,那麼對於指望的返回類型是C,可是實際返回類型是CSuper,也會發生錯誤。

因此可變參數化類型是不能變異的。

更多教程請參考 flydean的博客

相關文章
相關標籤/搜索