Эта страница документа относится к Scala 3 и может охватывать новые концепции, недоступные в Scala 2. Если не указано явно, все примеры кода на этой странице предполагают, что вы используете Scala 3.
Раньше в Scala было универсальное равенство (universal equality):
два значения любых типов можно было сравнивать друг с другом с помощью ==
и !=
.
Это произошло из-за того факта, что ==
и !=
реализованы в терминах метода equals
Java,
который также может сравнивать значения любых двух ссылочных типов.
Универсальное равенство удобно, но оно также опасно, поскольку подрывает безопасность типов.
Например, предположим, что после некоторого рефакторинга осталась ошибочная программа,
в которой значение y
имеет тип S
вместо правильного типа T
:
val x = ... // типа T
val y = ... // типа S, но должно быть типа T
x == y // результат проверки типов всегда будет 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, но он компилируется
Но в Scala 3 такие сравнения можно отключить.
При (а) импорте scala.language.strictEquality
или (б) использовании флага компилятора -language:strictEquality
это сравнение больше не компилируется:
import scala.language.strictEquality
val rover = Dog("Rover")
val fido = Dog("Fido")
println(rover == fido) // ошибка компиляции
// сообщение об ошибке компиляции:
// Values of types Dog and Dog cannot be compared with == or !=
Включение сравнений
Есть два способа включить сравнение с помощью класса типов CanEqual
.
Для простых случаев класс может выводиться (derive) от класса CanEqual
:
// Способ 1
case class Dog(name: String) derives CanEqual
Как вы вскоре увидите, когда нужна большая гибкость, вы также можете использовать следующий синтаксис:
// Способ 2
case class Dog(name: String)
given CanEqual[Dog, Dog] = CanEqual.derived
Любой из этих двух подходов позволяет сравнивать экземпляры Dog
друг с другом.
Более реалистичный пример
В более реалистичном примере представим, что есть книжный интернет-магазин и мы хотим разрешить или запретить сравнение бумажных, печатных и аудиокниг. В Scala 3 для начала необходимо включить многостороннее равенство:
// [1] добавить этот импорт или command line flag: -language:strictEquality
import scala.language.strictEquality
Затем создать объекты домена:
// [2] создание иерархии классов
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] создайте экземпляры класса типов, чтобы определить разрешенные сравнения.
// разрешено `PrintedBook == PrintedBook`
// разрешено `AudioBook == AudioBook`
given CanEqual[PrintedBook, PrintedBook] = CanEqual.derived
given CanEqual[AudioBook, AudioBook] = CanEqual.derived
// [4a] сравнение двух печатных книг разрешено
val p1 = PrintedBook("1984", "George Orwell", 1961, 328)
val p2 = PrintedBook("1984", "George Orwell", 1961, 328)
println(p1 == p2) // true
// [4b] нельзя сравнивать печатную книгу и аудиокнигу
val pBook = PrintedBook("1984", "George Orwell", 1961, 328)
val aBook = AudioBook("1984", "George Orwell", 2006, 682)
println(pBook == aBook) // ошибка компиляции
Последняя строка кода приводит к следующему сообщению компилятора об ошибке:
Values of types PrintedBook and AudioBook cannot be compared with == or !=
Вот как многостороннее равенство отлавливает недопустимые сравнения типов во время компиляции.
Включение “PrintedBook == AudioBook”
Если есть необходимость разрешить сравнение “печатной книги” (PrintedBook
) с аудио-книгой (AudioBook
),
то достаточно создать следующие два дополнительных сравнения равенства:
// разрешить `PrintedBook == AudioBook` и `AudioBook == PrintedBook`
given CanEqual[PrintedBook, AudioBook] = CanEqual.derived
given CanEqual[AudioBook, PrintedBook] = CanEqual.derived
Теперь можно сравнивать печатную книгу с аудио-книгой без ошибки компилятора:
println(pBook == aBook) // false
println(aBook == pBook) // false
Внедрите “equals”, чтобы они действительно работали
Хотя эти сравнения теперь разрешены, они всегда будут ложными,
потому что их методы equals
не знают, как проводить подобные сравнения.
Чтобы доработать сравнение, можно переопределить методы equals
для каждого класса.
Например, если переопределить метод equals
для AudioBook
:
case class AudioBook(
author: String,
title: String,
year: Int,
lengthInMinutes: Int
) extends Book:
// переопределить, чтобы разрешить сравнение AudioBook с 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 (работает из-за переопределенного `equals` в `AudioBook`)
println(pBook == aBook) // false
Книга PrintedBook
не имеет метода equals
, поэтому второе сравнение возвращает false
.
Чтобы включить это сравнение, достаточно переопределить метод equals
в PrintedBook
.
Вы можете найти дополнительную информацию о многостороннем равенстве в справочной документации.
Contributors to this page:
Contents
- Введение
- Возможности Scala
- Почему Scala 3?
- Почувствуй Scala
- Пример 'Hello, World!'
- REPL
- Переменные и типы данных
- Структуры управления
- Моделирование данных
- Методы
- Функции первого класса
- Одноэлементные объекты
- Коллекции
- Контекстные абстракции
- Верхнеуровневые определения
- Обзор
- Первый взгляд на типы
- Интерполяция строк
- Структуры управления
- Моделирование предметной области
- Инструменты
- Моделирование ООП
- Моделирование ФП
- Методы
- Особенности методов
- Main методы в Scala 3
- Обзор
- Функции
- Анонимные функции
- Параметры функции
- Eta расширение
- Функции высшего порядка
- Собственный map
- Создание метода, возвращающего функцию
- Обзор
- Пакеты и импорт
- Коллекции в Scala
- Типы коллекций
- Методы в коллекциях
- Обзор
- Функциональное программирование
- Что такое функциональное программирование?
- Неизменяемые значения
- Чистые функции
- Функции — это значения
- Функциональная обработка ошибок
- Обзор
- Типы и система типов
- Определение типов
- Параметризованные типы
- Пересечение типов
- Объединение типов
- Алгебраические типы данных
- Вариантность
- Непрозрачные типы
- Структурные типы
- Зависимые типы функций
- Другие типы
- Контекстные абстракции
- Методы расширения
- Параметры контекста
- Контекстные границы
- Given импорты
- Классы типов
- Многостороннее равенство
- Неявное преобразование типов
- Обзор
- Параллелизм
- Scala утилиты
- Сборка и тестирование проектов Scala с помощью Sbt
- Рабочие листы
- Взаимодействие с Java