С нуля до распределенных приложений.
Очень часто возникает необходимость в моделировании объектов со структурным равенством (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-классов автоматически получают модификатор 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
}