Что такое 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 — это удобный, лаконичный и мощный инструмент, особенно в контексте функционального стиля и сопоставления с образцом.