逆變性(Contravariance)


接續 共變性(Covariance) 的討論,接著來討論逆變性(Contravariance)。先來看看以下的簡單例子:
class Fruit
class Apple extends Fruit
class Node[T]

如 果B是A的子型態,而Node[A]被視為一種Node[B]型態,則稱Node具有逆變性(Contravariance)。在Scala中定義型態參數,預設是不具可變性(nonvariant),所以如果你使用以下的程式會編譯錯誤:
val f1 = new Node[Fruit]
val s1: Node[Apple] = f1 // 編譯錯誤, type mismatch

如果定義型態參數時,要讓型態參數具有逆變性,則可以標註 - 號。例如以下就可以通過編譯了:
class Fruit
class Apple extends Fruit
class Node[-T]

val f1 = new Node[Fruit]
val s1: Node[Apple] = f1

在上例中,型態參數標註了 - 號,Apple為Fruit的子型態,而Node[Fruit]被視為Node[Apple]的子型態,相較於 共變性(Covariance)來說,逆變性似乎違反直覺,但在某些情況下,確實是合理的需求。

舉例來說,你設計了以下的類別:
class Fruit(val price: Int, val weight: Int)
class Apple(override val price: Int,
override val weight: Int) extends Fruit(price, weight)
class Banana(override val price: Int,
override val weight: Int) extends Fruit(price, weight)

trait Comparator[T] {
def compare(t1: T, t2: T): Int
}

class Basket[T](things: T*) {
def sort(comparator: Comparator[T]) {
// 進行排序...
}
}

籃子(Basket)中可以置放各種物品,並可以傳入一個比較器(Comparator)進行排序。假設你分別在兩個籃子中放置了蘋果(Apple)與香蕉(Banana):
val b1 = new Basket(new Apple(20, 100), new Apple(25, 150))
val b2 = new Basket(new Banana(30, 200), new Banana(25, 250))

現在b1的型態為Basket[Apple],其比較方法為compare(comparator: Comparator[Apple]),而b2的型態為Basket[Banana],其比較方法為compare(comparator: Comparator[Banana])。如果你現在要實作一個水果(Fruit)比較器,比較水果的價格進行排序,希望可以同時適用於Basket[Apple]與Basket[Banana]:
val comparator = new Comparator[Fruit] { 
def compare(f1: Fruit, f2: Fruit) = f1.price - f2.price
}
b1.sort(comparator) // 編譯錯誤, type mismatch
b2.sort(comparator) // 編譯錯誤, type mismatch

b1的比較方法為compare(comparator: Comparator[Apple]),而你要傳入Comparator[Fruit]實例,所以編譯錯誤,b2 的比較方法為compare(comparator: Comparator[Banana]),而你要傳入Comparator[Fruit]實例,所以編譯錯誤,然而事實上,無論是Apple或 Banana,確實都是一種水果,也確實都有price成員,以Fruit型態取得price來進行比較其實是合理的。

如果在Comparator的型態參數上加上逆變標註 - 就可以通過編譯了:
class Fruit(val price: Int, val weight: Int)
class Apple(override val price: Int,
override val weight: Int) extends Fruit(price, weight)
class Banana(override val price: Int,
override val weight: Int) extends Fruit(price, weight)

trait Comparator[-T] {
def compare(t1: T, t2: T): Int
}

class Basket[T](things: T*) {
def sort(comparator: Comparator[T]) {
// 進行排序...
}
}

val comparator = new Comparator[Fruit] {
def compare(f1: Fruit, f2: Fruit) = f1.price - f2.price
}

val b1 = new Basket(new Apple(20, 100), new Apple(25, 150))
val b2 = new Basket(new Banana(30, 200), new Banana(25, 250))

b1.sort(comparator)
b2.sort(comparator)

如果有兩個以上的型態參數,則可分別標註可變性,例如 scala.collection.immutable.Map[A, +B] 就是一例,而 scala.Function1[-T1, +R] 則是同時標註逆變性與正變性的例子。一級函式(First-class function) 中介紹過函式常量(Function literal),一個函式常量A=>B,其實會是Function1[A, B]的實例。也就是說,以下是正確的語法:
class Parent
class Child extends Parent

def test(f: Child => Parent) = {}

val f1 = (c: Child) => new Parent
val f2 = (p: Child) => new Child
val f3 = (p: Parent) => new Parent
test(f1)
test(f2)
test(f3)

 一個實際的應用例子如下:
class Fruit(val price: Int, val weight: Int) {
override def toString = "Fruit(" + price + ", " + weight + ")"
}
class Apple(override val price: Int,
override val weight: Int) extends Fruit(price, weight) {
override def toString = "Apple(" + price + ", " + weight + ")"
}
class Banana(override val price: Int,
override val weight: Int) extends Fruit(price, weight) {
override def toString = "Banana(" + price + ", " + weight + ")"
}

class Basket[T](things: T*) {
def show(info: T => Any) = {
for(thing <- things) {
println(info(thing))
}
}
}

在上例中,你為Basket設計了一個show()方法,可以讓使用者自行決定如何取得資訊,這要傳入一個函式物件,你可以如下設計所要傳入的函式:
def description(f: Fruit) = f.toString
def price(f: Fruit) = f.price
def weight(f: Fruit) = f.weight

val b1 = new Basket(new Apple(20, 100), new Apple(25, 150))
val b2 = new Basket(new Banana(30, 200), new Banana(25, 250))

// 顯示蘋果籃各項資訊
b1.show(description)
b1.show(price)
b1.show(weight)

// 顯示香蕉籃各項資訊
b2.show(description)
b2.show(price)
b2.show(weight)

b1 的型態是Basket[Apple],而顯示用的方法為show(info: Apple => Any),由於Function1[-T1, R] 的標註,傳入的函式參數可以逆變,而傳回值可以正變,所以你所設計的description()、price()與weight()函式可以適用b1的 show()方法,也就是說,只要傳入的是一種水果(Fruit),而傳回值可以是任何資訊(Any)的函式,都可以給show()方法使用。b2可以接 受description()、price()與weight()函式也是相同。