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 方法。

你可以在参考文档中找到有关 多重相等性 的更多信息。

本页的贡献者