Введение в Scala.

С нуля до распределенных приложений.

Кейс-классы и сопоставления

Проблемы сравнения

Очень часто возникает необходимость в моделировании объектов со структурным равенством (structural equality). То есть, таких объектов, которые будут сравниваться по значению, а не по ссылке, как, к примеру, целые числа или строки*.

Смоделируем подобное поведение для нового класса рациональных чисел.

class Rational(val num: Int, val den: Int) {

  assert(den != 0)

  def unary_- = new Rational(-num, den) // так объявляется унарный оператор -

  def *(other: Rational) = new Rational(num * other.num, den * other.den)
  def /(other: Rational) = new Rational(num / other.num, den / other.den)

  def +(other: Rational) = new Rational(num * other.den + other.num * den, den * other.den)
  def -(other: Rational) = this + (-other)
}

Сравним два объекта этого класса:

scala> new Rational(1, 2) == new Rational(1, 2)
<console>:9: warning: comparing a fresh object using '==' will always yield false
              new Rational(1, 2) == new Rational(1, 2)
                                 ^
res7: Boolean = false

Т.к. сравнение при помощи оператора == всегда вызывает функцию equals(arg: Any) на объекте, мы можем исправить код добавив в класс Rational следующую функцию:

override def equals(other: Any) = other.isInstanceOf[Rational] && {
  val otherRational = other.asInstanceOf[Rational]
  num == otherRational.num && den == otherRational.den
}

Но это не решает всех наших проблем.

Что произойдет, если мы поместим наши объекты в ассоциативный массив?

scala> scala.collection.mutable.Map(new Rational(1, 2) -> "A", new Rational(2, 3) -> "B")
res35: scala.collection.mutable.Map[Rational,String] = Map(Rational@162c1dfb -> B, Rational@7fda2001 -> A)

scala> res35.get(new Rational(1, 2))
res36: Option[String] = None

Это просходит потому, что при поиске многие структуры данных используют поле hashCode, а не equals.

В данном случае не сложно переопределить и его.

override def hashCode = (num / den.toDouble).hashCode

Это исправило предыдущий код, но не всегда легко переопределить equals и hashCode и это легко забыть.

Case-классы

Эту проблему решают case-классы. Они созданы для моделирования объектов со структурным равенством.

Пример:

// поля case-классов автоматически получают модификатор public
scala> case class Person(name: String, age: Int)
defined class Person

scala> val p1 = Person("Tagir", 30) // ключевое слово new не нужно
p1: Person = Person(Tagir,30)

scala> val p2 = Person("Tagir", 30)
p2: Person = Person(Tagir,30)

scala> val p3 = Person("Timur", 31)
p3: Person = Person(Timur,31)

scala> p1 == p2 // true
res39: Boolean = true

scala> val m = Map(p1 -> 85, p3 -> 75)
m: scala.collection.immutable.Map[Person,Int] = Map(Person(Tagir,30) -> 85, Person(Timur,31) -> 75)

scala> m(Person("Tagir", 30))
res40: Int = 85

Case-классы получают автоматически сгенерированные методы equals и hashCode, а не просто наследуют их от базового класса Any.

Но это еще не все. Другие сгенерированные методы этих классов позволяют использовать case классы в одном из самых интересных синтаксических возможностей языка Scala, сопоставлении с образцом (pattern matching).

Сопоставление с образцом

Case-классы (и вообще все классы у которых есть методы unapply или unapplySeq) можно сопоставлять с образцом разбирая на части. Эдакий switch на стероидах.

<объект> match {
  case <паттерн1> => <результат>
  case <паттерн2> => <результат>
  ...
  case _ => <default>
}

Пример:

case class Person(name: String, age: Int)
class Employee
case class Engineer(id: Person) extends Employee
case class Manager(id: Person, reports: List[Employee]) extends Employee

// _ в данном контексте означает любое значение
def isDave(p: Employee) = p match {
  case Engineer(Person("Dave", _)) => true
  case Manager(Person("Dave", _), _) => true
  case _ => false
}

def isManagerOver50(p: Employee) = p match {
  case Manager(Person("Dave", age), _) if age > 50 => true
  case _ => false
}

def numberOfReports(employee: Employee): Int = employee match {
  case Manager(_, reports) => reports.length
  case _ => 0
}