共變性(Covariance)


如果你定義了以下的類別:
class Node[T](val value: T, val next: Node[T])

如果在以下的例子中:
class Fruit
class Apple extends Fruit {
override def toString = "Apple"
}
class Banana extends Fruit {
override def toString = "Banana"
}

val apple = new Node(new Apple, null)
val fruit: Node[Fruit] = apple // 編譯錯誤,type mismatch

在 範例中,apple的型態是Node[Apple],而fruit的型態為Node[Fruit],你將apple所參考的物件給fruit參考,那麼Node[Apple]該是一種Node[Fruit]呢?在上例中編譯器給你的答案為「不是」!

如 果B是A的子型態,而Node[B]被視為一種Node[A]型態,則稱Node具有共變性(Covariance)或有彈性的(flexible)。如 果Node[A]被視為一種Node[B]型態,則稱Node具有逆變性(Contravariance)。如果不具共變性或逆變性,則Node是不可變 的(nonvariant)或嚴謹的(rigid)。

如果要讓型態參數具有共變性,則在定義型態參數時,可以加上+標註。例如:
class Node[+T](val value: T, val next: Node[T])
class Fruit
class Apple extends Fruit {
override def toString = "Apple"
}
class Banana extends Fruit {
override def toString = "Banana"
}

val apple = new Node(new Apple, null)
val fruit: Node[Fruit] = apple // 編譯通過

何時讓型態參數具有共變性?假設你設計了以下的函式:
def show(n: Node[Fruit]) {
var node: Node[Fruit] = n
do {
println(node.value)
node = node.next
} while(node != null)
}

val apple1 = new Node(new Apple, null)
val apple2 = new Node(new Apple, apple1)
val apple3 = new Node(new Apple, apple2)

val banana1 = new Node(new Banana, null)
val banana2 = new Node(new Banana, banana1)

show(apple3)
show(banana2)

你的目的是可以顯示所有的水果節點,如果上面的Node類別設計時不具共變性,則這個函式無法運作,如果Node類別設計時具有共變性,則這個函式就可以顯示Node[Apple]也可以顯示Node[Banana]。

注意!一旦你將型態參數標註為協變,就不可以用它來宣告方法的參數型態。例如:
class Node[+T](val value: T, val next: Node[T]) {
def replace(value: T) = new Node[T](value, next) // 編譯錯誤
}

假設上例可以通過編譯好了,那麼以下的程式碼就會不合理:
val apple1 = new Node(new Apple, null)
val apple2 = new Node(new Apple, apple1)
val fruit1: Node[Fruit] = apple2
val fruit2 = fruit1.replace(new Banana) 

fruit1所參考的實際上就是apple2的實例,而apple2所參考的是Node參數化為Apple後的物件,其實相當於以下的實例:
class Node[Apple](val value: Apple, val next: Node[Apple]) {
    def replace(value: Apple) = new Node[Apple](value, next) 
}

所以你透過fruit1操作replace()方法時,相當於要將Banana實例給型態為Apple的value參考,這顯然是不合理!

如果你的目的是,是要在蘋果節點前放上香蕉節點,則方式是在定義方法時,另外提供型態參數,例如:
class Node[+T](val value: T, val next: Node[T]) {
def replace[U >: T](value: U) = new Node[U](value, next)
}

val apple1 = new Node(new Apple, null)
val apple2 = new Node(new Apple, apple1)
val fruit1: Node[Fruit] = apple2
val fruit2 = fruit1.replace[Fruit](new Banana)
println(fruit2.value) // Banana
println(fruit2.next.value) // Apple

上面的範例可以通過編譯,因為fruit1所參考的實際上就是apple2的實例,而apple2所參考的是Node參數化為Apple後的物件,其實相當於以下的實例:
class Node[Apple](val value: Apple, val next: Node[Apple]) {
    def replace[U >: Apple](value: U) = new Node[U](value, next)
}

所 以範例中使用replace()時,其實是另又作了一次型態參數化,結果是建立了Node[Fruit]實例,Banana可以給Node[Fruit] 實例的value參考(因為是Fruit型態)沒有問題(事實上,Apple的父類別只有Fruit,所以範例中repace()方法前的[Fruit] 可以使用類型推斷方法省略)。

注意!如果型態參數支援共變性,則也不可以用型態參數宣告var成員。例如:
class Node[+T] {
var value: T = _ // 編譯錯誤
}

屬性存取方法 說過,一個var成員,事實上會是一對存取方法,也就是說,上例相當於以上的宣告:
class Node[+T] {
    private[this] var v: T = _
    def value: T = v
    def value_=(v: T) { this.v = v } 
}

也就是說,var成員會產生一個設值方法,使用型態參數宣告傳入的參數型態,所以其實是上述原則「將型態參數標註為協變,就不可以用它來宣告方法的參數型態」的一個特例。