參數化型態的物件相等性


重新定義 equals() 方法 討論過如何重新定義equals()方法,如果定義類別時使用了參數化型態,則有幾個地方要注意的,例如:
class Basket[T](val things: T*) {
override def equals(a : Any) = a match {
case that: Basket[T] => this.things.toArray
deepEquals
that.things.toArray
case _ => false
}
}

如果你編譯這個程式,會發現以下的警示訊息:
warning: there were unchecked warnings; re-run with -unchecked for details

在編譯時加上-unchecked引數,可以看到詳細的警示訊息內容:
warning: non variable type-argument T in type pattern is unchecked since it is eliminated by erasure
      case that: Basket[T] => this.things.toArray deepEquals that.things.toArray

大 意是說,你在程式中Basket[T]的型態參數T是沒有用的,因為Scala的型態參數所採用的是型態抹除,模式比對時,僅比對Basket型態,不會 針對當中的型態參數真正之型態進行比對,程式仍可以運行,不過警告你不要期待可以比對Basket[Int]、Basket[String]等的差別。

如果不想看到警示訊息,可以使用佔位字元的方式重新撰寫:
class Basket[T](val things: T*) {
override def equals(a : Any) = a match {
case that: Basket[_] => this.things.toArray deepEquals
that.things.toArray
case _ => false
}
}

現在你可以使用==來比較兩個Basket是不是相同了:
val b1 = new Basket(1, 2)
val b2 = new Basket(1, 2)
val b3 = new Basket(2, 2)
val b4 = new Basket("1", "2")
println(b1 == b2) // true
println(b1 == b3) // false
println(b1 == b4) // false

看起來不錯,不過來看看下面這個例子:
val b1 = new Basket[Int]
val b2 = new Basket[String]
println(b1 == b2) // true

Basket[Int]與Basket[String]若是視作不同的型態,則b1與b2應視為不相等。不過現在實際上,兩個Basket都沒有東西,由於Scala採用型態抹除的方式,結果就是認為在這種情況下,b1與b2是相等的。其實這也可以在以下的例子中看到:
val l1: List[Int] = Nil    
val l2: List[String] = Nil
println(l1 == l2) // true

List[Int]、List[String]是不同的型態,但Scala這麼想,l1、l2都是空串列Nil,那它們不就是相等的嗎?同樣的道理,Basket[Int]與Basket[String]是不同的型態沒錯,但你的Basket定義就是比較是不是籃子(Basket[_]),以及實際籃子中放的東西是什麼,籃子都沒有東西,結果應該就是true。

以下考慮繼承關係後的equals()、hashCode定義,同樣也注意到,isInstanceOf只能測試Basket型態,無法測試型態參數真正的型態:
class Basket[T](val things: T*) {
override def equals(a : Any) = a match {
case that: Basket[_] => (that canEquals this) &&
(this.things.toArray deepEquals
that.things.toArray)
case _ => false
}
def canEquals(other: Any) = other.isInstanceOf[Basket[_]]
override def hashCode = 41 * (1 /: things.toArray) {_ * 41 + _.hashCode}
+ things.hashCode
}