Что такое case class и зачем она нужна?
В Scala case class — это особый вид класса, предназначенный для работы с неизменяемыми структурами данных, сопоставления с образцом (pattern matching), и удобного создания экземпляров без явного использования new. Это мощный инструмент функционального программирования, широко используемый в Scala для моделирования ADT (алгебраических типов данных), DTO, сообщений между акторами и других чисто функциональных структур.
Объявление case class
case class Person(name: String, age: Int)
После этого можно создавать объекты без ключевого слова new:
val john = Person("John", 30)
Основные особенности case class
1. Автоматическое определение методов
Когда вы объявляете case class, компилятор автоматически создаёт:
-
apply-метод для удобного создания экземпляров;
-
unapply-метод для pattern matching;
-
toString, hashCode, equals;
-
copy-метод;
-
реализацию Product и Serializable.
2. Immutable by default (неизменяемость)
Аргументы конструктора автоматически становятся val. Это означает, что их нельзя изменить после создания:
val p = Person("Alice", 25)
// p.name = "Bob" // ошибка — поля val
3. Сравнение по значению, а не по ссылке
Обычные классы в Scala сравниваются по ссылке (как в Java). case class сравниваются по значению:
val a = Person("Alice", 25)
val b = Person("Alice", 25)
a == b // true
4. Pattern Matching
case class прекрасно работает с match:
def greet(person: Person): String = person match {
case Person("Alice", \_) => "Hi, Alice!"
case Person(name, age) => s"Hello, $name, age $age"
}
Здесь используется автоматически сгенерированная функция unapply, которая извлекает значения полей.
5. Метод copy
Можно копировать объекты с изменением отдельных полей:
val john = Person("John", 30)
val olderJohn = john.copy(age = 31)
Это особенно удобно для работы с immutable-объектами.
Примеры использования
Алгебраические типы данных (ADT)
sealed trait + case class часто используют для описания ADT:
sealed trait Expr
case class Num(value: Int) extends Expr
case class Add(left: Expr, right: Expr) extends Expr
case class Mul(left: Expr, right: Expr) extends Expr
def eval(expr: Expr): Int = expr match {
case Num(v) => v
case Add(l, r) => eval(l) + eval(r)
case Mul(l, r) => eval(l) \* eval(r)
}
Это позволяет легко расширять код, сохраняя типобезопасность и удобочитаемость.
Отличия от обычных классов
Особенность | class | case class |
---|---|---|
apply/unapply | вручную | автоматически |
--- | --- | --- |
equals / hashCode | по ссылке | по значению |
--- | --- | --- |
toString | простой (Class@1234) | детализированное (Person(...)) |
--- | --- | --- |
copy | нет | есть |
--- | --- | --- |
pattern matching | не поддерживается | поддерживается |
--- | --- | --- |
поля конструктора | var или val — по выбору | val по умолчанию |
--- | --- | --- |
Расширение функционала
Можно добавлять методы:
case class Rectangle(width: Int, height: Int) {
def area: Int = width \* height
}
Поддержка вложенных структур
Можно использовать case class рекурсивно:
case class Tree(value: Int, left: Option\[Tree\], right: Option\[Tree\])
Модификаторы и ограничения
-
case class не может быть abstract, но может реализовать trait;
-
можно использовать case object — синглтон с теми же преимуществами:
sealed trait Status
case object Success extends Status
case object Failure extends Status
Пример DTO
case class Order(id: Long, amount: Double)
Такой класс можно передавать между слоями приложения, он удобен для сериализации.
Пример сериализации и JSON
Вместе с библиотекой circe, play-json или spray-json можно легко сериализовать/десериализовать case class:
import play.api.libs.json._
case class Book(title: String, price: Double)
implicit val format = Json.format\[Book\]
val json = Json.toJson(Book("Scala", 45.0))
Использование в Akka
Сообщения между акторами в Akka часто оформляют как case class:
case class Greet(name: String)
Это даёт:
-
удобное сравнение сообщений;
-
возможность сопоставления;
-
сериализацию.
Уточнение по доступу и производительности
-
Несмотря на удобство, case class не всегда подходят для тяжёлых объектов с большим количеством мутабельных данных.
-
Они не предназначены для наследования между case class, но могут реализовать общий trait.
-
Поддерживается equals, что делает их удобными для использования в коллекциях как ключи.
Пример вложенной структуры с pattern matching
case class Company(name: String, employees: List\[Person\])
val acme = Company("ACME", List(Person("Bob", 25), Person("Alice", 30)))
acme match {
case Company("ACME", List(Person(\_, age), \_\*)) if age > 20 =>
println("Есть взрослые сотрудники")
}
Когда не стоит использовать case class
-
Если требуется сложное поведение в конструкторе — лучше обычный class.
-
Если объект изменяемый — использовать var в обычном классе.
-
Для heavy-duty бизнес-логики с многослойным наследованием лучше использовать trait + class.
Таким образом, case class в Scala — это удобный, лаконичный и мощный инструмент, особенно в контексте функционального стиля и сопоставления с образцом.