Scala 3 — 書籍

多重宇宙等價性

語言
此文件頁面專屬於 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 方法。例如,當你覆寫 AudioBookequals 方法時

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

現在你可以比較 AudioBookPrintedBook

println(aBook == pBook)   // true (works because of `equals` in `AudioBook`)
println(pBook == aBook)   // false

目前,PrintedBook 書籍沒有 equals 方法,所以第二次比較會傳回 false。若要啟用該比較,只要覆寫 PrintedBook 中的 equals 方法即可。

您可以在參考文件檔中找到有關多重相等性的更多資訊。

此頁面的貢獻者