2014-02-18 18 views
2

查看源爲List.scala理解列表[+ A]爲協方差

sealed abstract class List[+A] extends ... 

    ... 

    def isEmpty: Boolean 
    def head: A 
    def tail: List[A] 

List[+A]是基於所述協變+A。這是否意味着可以創建一個List[T],其中T可以是類型本身或其任何子類?

例如:

scala> trait Kid 
defined trait Kid 

scala> case class Boy(name: String) extends Kid 
defined class Boy 

scala> case class Girl(name: String) extends Kid 
defined class Girl 

scala> val list: List[Kid] = List(Boy("kevin"), Girl("sally")) 
list: List[Kid] = List(Boy(kevin), Girl(sally)) 

觀察到headtail的類型分別是AList[A]。一旦我們定義了List[+A],那麼headtailA也是協變?

我讀過這個StackOverflow answer 3或4次,但我還不明白。

+0

示例代碼根本不依賴於'List'協方差。你能否擴大你的問題/例子,使其更清晰? –

+0

基本上我對方差感到困惑,並提出一個問題來挑起關於這個話題的答案。弗拉基米爾用他的回答幫了我很多。 –

回答

8

您的示例與方差無關。此外,頭部和尾部也與變化無關。

scala> val list: List[Kid] = List(Boy("kevin"), Girl("sally")) 
list: List[Kid] = List(Boy(kevin), Girl(sally)) 

即使List不協變的,因爲Scala會自動推斷的BoyGirl常見的超這樣的工作,也就是Kid,並在右側的表達式的類型將是List[Kid],到底是什麼你需要在左邊。

以下,然而,工作,因爲java.util.List不是協變(這是不變的,因爲它是Java類型):

scala> import java.util.{List => JList, Arrays} 
import java.util.{List=>JList, Arrays} 

scala> trait Kid 
defined trait Kid 

scala> case class Boy(name: String) extends Kid 
defined class Boy 

scala> val list1 = Arrays.asList(Boy("kevin"), Boy("bob")) 
list1: java.util.List[Boy] = [Boy(kevin), Boy(bob)] 

scala> val list2: JList[Kid] = list1 
<console>:12: error: type mismatch; 
found : java.util.List[Boy] 
required: java.util.List[Kid] 
Note: Boy <: Kid, but Java-defined trait List is invariant in type E. 
You may wish to investigate a wildcard type such as `_ <: Kid`. (SLS 3.2.10) 
     val list2: JList[Kid] = list1 
          ^

Arrays.asList方法的簽名是這樣的:

def asList[T](args: T*): java.util.List[T] 

由於java.util.List[T]不變,因此不可能將JList[Boy]list1)指定爲JList[Kid]list2)。有一個原因:如果可以,那麼因爲JList是可變的,您還可以添加任何延伸Kid(不僅是Boy)的任何內容到同一個列表中,以突破類型安全性。

在另一方面,scala.List工作在完全相同的情況:

scala> val list1 = List(Boy("kevin"), Boy("bob")) 
list1: List[Boy] = List(Boy(kevin), Boy(bob)) 

scala> val list2: List[Kid] = list1 
list2: List[Kid] = List(Boy(kevin), Boy(bob)) 

這是因爲scala.List在其類型參數協變。請注意,協變List類型的工作方式就好像List[Boy]List[Kid]的子類型,非常類似於您可以將所有內容都分配給類型爲Any的變量的情況,因爲每個其他類型都是Any的子類型。這是非常有用的類比。

逆變函數的工作方式非常類似,但在其他方向。考慮這個特質:

trait Predicate[-T] { 
    def apply(obj: T): Boolean 
} 

object Predicate { 
    // convenience method to convert functions to predicates 
    def apply[T](f: (T) => Boolean) = new Predicate[T] { 
    def apply(obj: T) = f(obj) 
    } 
} 

注意-T參數:它是一個逆變註釋,就是Predicate[T]被定義在它的唯一類型參數進行逆變。

回想一下,對於協變列表List[Boy]List[Kid]的子類型。那麼,對於逆變斷言它在相反的方向:Predicate[Kid]Predicate[Boy]一個亞型,所以你可以Predicate[Kid]類型的值賦給Predicate[Boy]類型的變量:

scala> val pred1: Predicate[Kid] = Predicate { kid => kid.hashCode % 2 == 0 } 
pred1: Predicate[Kid] = [email protected] 

scala> val pred2: Predicate[Boy] = pred1 
pred2: Predicate[Boy] = [email protected] 

如果Predicate[T]不逆變,我們將不能將pred1分配給pred2,儘管它是完全合法和安全的:顯然,在超類型上定義的謂詞可以很容易地處理子類型。


總之,方差會影響參數化類型之間的類型兼容性。 List是協變,這樣就可以List[Boy]類型的值分配給List[Kid]類型的變量(實際上,對於任何T延伸S,可以List[T]類型的值分配給List[S]類型的變量)。

在另一方面,因爲,Predicate是逆變,可以分配給Predicate[Kid]Predicate[Boy](即,用於擴展S任何T,可以Predicate[S]類型的值分配給Predicate[T]類型的變量)。

如果某個類型在其類型參數中不變,則上述任何一項都不能完成(如JList所示)。

注意參數類型和參數之間的對應關係:

T <: S ===> List  [T] <: List  [S] (covariance) 
T <: S ===> Predicate[S] <: Predicate[T] (contravariance) 

這就是爲什麼第一個效果就是所謂的理由*合作*方差(T <: S在左邊,在右邊
..T.. <: ..S..)第二個是* contra *方差(左側爲T <: S,右側爲..S.. <: ..T..)。

是否使自己的參數化類型協變或逆變或不變取決於您的類職責。如果只有返回泛型類型的值,那麼使用協方差是有意義的。例如,List[T]僅包含返回T,從不接受T作爲參數的方法,因此爲了增加表示力而使其協變是安全的。這種參數化類型可以稱爲生產者

如果你的類只接受泛型類型作爲參數的值,而不是返回他們(酷似Predicate以上具有單一方法def apply(obj: T): Boolean),那麼你可以放心地讓它逆變。這樣的參數化類型可以叫做消費者

如果你的類都接受和返回泛型類型的值,即它既是生產者又是消費者,那麼你別無選擇,只能在這個泛型中保留類不變類型參數。

這個成語通常被稱爲「佩奇」(「製片人extends,消費者super」),因爲方差的註解是在Java編寫extendssuper

+0

謝謝你的詳細解答。你能給我一個關於謂詞的例子嗎?我仍然不理解參數和協方差在返回類型中的逆變。 –

+0

@KevinMeredith,我用逆轉例子更新了答案。 –

+0

據我瞭解 - 你指出'List [Kid]'接受一個'Boy'類型:'List [Kid](Boy(「kevin」))與方差無關。簡單地說,可以在定義父類型的地方使用子類型 - 基本的OO。所以,我仍然對此感到困惑的是:對於List [A],這意味着類List具有協變類型A.協變意味着可以使用A或其子類型。但是,鑑於Scala的OO特性,我們不能已經使用A的子類型嗎?在我看來,「方差」可以改變哪些類型和子類型可以使用。 –