在變與不變之間
|
|
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)。
Scala 中如果要讓型態參數具有共變性,則在定義型態參數時,可以加上+標註。例如:
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)
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)
}
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)
}
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 }
}
private[this] var v: T = _
def value: T = v
def value_=(v: T) { this.v = v }
}
也就是說,var成員會產生一個設值方法,使用型態參數宣告傳入的參數型態,所以其實是上述原則「將型態參數標註為協變,就不可以用它來宣告方法的參數型態」的一個特例。
接著來討論逆變性(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()函式也是相同。
迴響: