此文件頁面專屬於 Scala 3,且可能涵蓋 Scala 2 中沒有的新概念。除非另有說明,否則此頁面中的所有程式碼範例都假設您使用的是 Scala 3。
以前,Scala 具有通用等價性:可以使用 ==
和 !=
將任何類型的兩個值進行比較。這是因為 ==
和 !=
是根據 Java 的 equals
方法實作的,該方法也可以比較任何兩個參考類型的值。
通用相等性很方便,但它也具有危險性,因為它會破壞類型安全性。例如,讓我們假設在某次重構後,你遺留了一個錯誤的程式,其中一個值 y
的類型為 S
,而不是正確的類型 T
val x = ... // of type T
val y = ... // of type S, but should be T
x == y // typechecks, will always yield false
如果 y
與其他類型為 T
的值進行比較,程式仍然會進行類型檢查,因為所有類型的值都可以相互比較。但它可能會產生意外的結果,並在執行時失敗。
一個類型安全的程式語言可以做得更好,而多重相等性是一種選擇加入的方式,可以讓通用相等性更安全。它使用二元類型類別 CanEqual
來表示兩個給定類型的值可以相互比較。
允許比較類別實例
在 Scala 3 中,預設情況下你仍然可以建立這樣的相等性比較
case class Cat(name: String)
case class Dog(name: String)
val d = Dog("Fido")
val c = Cat("Morris")
d == c // false, but it compiles
但是使用 Scala 3,你可以停用此類比較。透過 (a) 匯入 scala.language.strictEquality
或 (b) 使用 -language:strictEquality
編譯器標記,此比較將不再編譯
import scala.language.strictEquality
val rover = Dog("Rover")
val fido = Dog("Fido")
println(rover == fido) // compiler error
// compiler error message:
// Values of types Dog and Dog cannot be compared with == or !=
啟用比較
有兩種方法可以使用 Scala 3 CanEqual
類別類型來啟用此比較。對於像這樣簡單的案例,你的類別可以衍生 CanEqual
類別
// Option 1
case class Dog(name: String) derives CanEqual
正如你將在稍後看到的,當你需要更大的靈活性時,你也可以使用此語法
// Option 2
case class Dog(name: String)
given CanEqual[Dog, Dog] = CanEqual.derived
這兩種方法現在都能讓 Dog
實例彼此比較。
更實際的範例
在一個更實際的範例中,想像你有一個線上書店,並想要允許或不允許比較實體印刷書和有聲書。使用 Scala 3,你可以從啟用多重宇宙等值開始,如前一個範例所示
// [1] add this import, or this command line flag: -language:strictEquality
import scala.language.strictEquality
然後照常建立你的網域物件
// [2] create your class hierarchy
trait Book:
def author: String
def title: String
def year: Int
case class PrintedBook(
author: String,
title: String,
year: Int,
pages: Int
) extends Book
case class AudioBook(
author: String,
title: String,
year: Int,
lengthInMinutes: Int
) extends Book
最後,使用 CanEqual
定義你想要允許哪些比較
// [3] create type class instances to define the allowed comparisons.
// allow `PrintedBook == PrintedBook`
// allow `AudioBook == AudioBook`
given CanEqual[PrintedBook, PrintedBook] = CanEqual.derived
given CanEqual[AudioBook, AudioBook] = CanEqual.derived
// [4a] comparing two printed books works as desired
val p1 = PrintedBook("1984", "George Orwell", 1961, 328)
val p2 = PrintedBook("1984", "George Orwell", 1961, 328)
println(p1 == p2) // true
// [4b] you can’t compare a printed book and an audiobook
val pBook = PrintedBook("1984", "George Orwell", 1961, 328)
val aBook = AudioBook("1984", "George Orwell", 2006, 682)
println(pBook == aBook) // compiler error
最後一行程式碼會產生這個編譯器錯誤訊息
Values of types PrintedBook and AudioBook cannot be compared with == or !=
這就是多重宇宙等值在編譯時捕捉非法類型比較的方式。
啟用「PrintedBook == AudioBook」
這會如預期般運作,但在某些情況下,你可能想要允許比較實體書和有聲書。當你想要這麼做時,建立這兩個額外的等值比較
// allow `PrintedBook == AudioBook`, and `AudioBook == PrintedBook`
given CanEqual[PrintedBook, AudioBook] = CanEqual.derived
given CanEqual[AudioBook, PrintedBook] = CanEqual.derived
現在你可以比較實體書和有聲書,而不會產生編譯器錯誤
println(pBook == aBook) // false
println(aBook == pBook) // false
實作「equals」讓它們真正運作
雖然現在允許這些比較,但它們永遠都會是 false
,因為它們的 equals
方法不知道如何進行這些比較。因此,解決方案是覆寫每個類別的 equals
方法。例如,當你覆寫 AudioBook
的 equals
方法時
case class AudioBook(
author: String,
title: String,
year: Int,
lengthInMinutes: Int
) extends Book:
// override to allow AudioBook to be compared to PrintedBook
override def equals(that: Any): Boolean = that match
case a: AudioBook =>
this.author == a.author
&& this.title == a.title
&& this.year == a.year
&& this.lengthInMinutes == a.lengthInMinutes
case p: PrintedBook =>
this.author == p.author && this.title == p.title
case _ =>
false
現在你可以比較 AudioBook
和 PrintedBook
println(aBook == pBook) // true (works because of `equals` in `AudioBook`)
println(pBook == aBook) // false
目前,PrintedBook
書籍沒有 equals
方法,所以第二次比較會傳回 false
。若要啟用該比較,只要覆寫 PrintedBook
中的 equals
方法即可。
您可以在參考文件檔中找到有關多重相等性的更多資訊。