Scala Language
Классы классов
Поиск…
Синтаксис
- case class Foo () // Классы классов без параметров должны иметь пустой список
- класс case Foo (a1: A1, ..., aN: AN) // Создаем класс case с полями a1 ... aN
- case object Bar // Создание класса case singleton
Равномерность класса
Одна функция, предоставляемая бесплатно по классам классов, - это метод автогенерации equals
значений, который проверяет равенство значений всех отдельных полей-членов вместо проверки ссылочного равенства объектов.
С обычными классами:
class Foo(val i: Int)
val a = new Foo(3)
val b = new Foo(3)
println(a == b)// "false" because they are different objects
С классами классов:
case class Foo(i: Int)
val a = Foo(3)
val b = Foo(3)
println(a == b)// "true" because their members have the same value
Сгенерированные артефакты кода
Модификатор case
заставляет компилятор Scala автоматически генерировать общий шаблонный код для класса. Внедрение этого кода вручную является утомительным и источником ошибок. Следующее определение класса case:
case class Person(name: String, age: Int)
... будет автоматически генерироваться следующий код:
class Person(val name: String, val age: Int)
extends Product with Serializable
{
def copy(name: String = this.name, age: Int = this.age): Person =
new Person(name, age)
def productArity: Int = 2
def productElement(i: Int): Any = i match {
case 0 => name
case 1 => age
case _ => throw new IndexOutOfBoundsException(i.toString)
}
def productIterator: Iterator[Any] =
scala.runtime.ScalaRunTime.typedProductIterator(this)
def productPrefix: String = "Person"
def canEqual(obj: Any): Boolean = obj.isInstanceOf[Person]
override def hashCode(): Int = scala.runtime.ScalaRunTime._hashCode(this)
override def equals(obj: Any): Boolean = this.eq(obj) || obj match {
case that: Person => this.name == that.name && this.age == that.age
case _ => false
}
override def toString: String =
scala.runtime.ScalaRunTime._toString(this)
}
Модификатор case
также создает объект-компаньон:
object Person extends AbstractFunction2[String, Int, Person] with Serializable {
def apply(name: String, age: Int): Person = new Person(name, age)
def unapply(p: Person): Option[(String, Int)] =
if(p == null) None else Some((p.name, p.age))
}
При применении к object
модификатор case
имеет похожие (хотя и менее драматические) эффекты. Здесь основными преимуществами являются реализация toString
и значение hashCode
, которое согласовано между процессами. Обратите внимание, что объекты case (правильно) используют ссылочное равенство:
object Foo extends Product with Serializable {
def productArity: Int = 0
def productIterator: Iterator[Any] =
scala.runtime.ScalaRunTime.typedProductIterator(this)
def productElement(i: Int): Any =
throw new IndexOutOfBoundsException(i.toString)
def productPrefix: String = "Foo"
def canEqual(obj: Any): Boolean = obj.isInstanceOf[this.type]
override def hashCode(): Int = 70822 // "Foo".hashCode()
override def toString: String = "Foo"
}
По-прежнему можно вручную реализовать методы, которые в противном случае предоставлялись модификатором case
как в самом классе, так и в его сопутствующем объекте.
Основы работы в классе
По сравнению с обычными классами - нотация классов случаев дает несколько преимуществ:
Все аргументы конструктора являются
public
и могут быть доступны для инициализированных объектов (как правило, это не так, как показано здесь):case class Dog1(age: Int) val x = Dog1(18) println(x.age) // 18 (success!) class Dog2(age: Int) val x = new Dog2(18) println(x.age) // Error: "value age is not a member of Dog2"
Он предоставляет реализацию для следующих методов:
toString
,equals
,hashCode
(на основе свойств),copy
,apply
и неunapply
:case class Dog(age: Int) val d1 = Dog(10) val d2 = d1.copy(age = 15)
Он обеспечивает удобный механизм для сопоставления шаблонов:
sealed trait Animal // `sealed` modifier allows inheritance within current build-unit only case class Dog(age: Int) extends Animal case class Cat(owner: String) extends Animal val x: Animal = Dog(18) x match { case Dog(x) => println(s"It's a $x years old dog.") case Cat(x) => println(s"This cat belongs to $x.") }
Классы классов и иммунитет
Компилятор Scala префикс каждого аргумента в списке параметров по умолчанию с val
. Это означает, что по умолчанию классы case неизменяемы. Каждому параметру присваивается метод доступа, но методы мутатора отсутствуют. Например:
case class Foo(i: Int)
val fooInstance = Foo(1)
val j = fooInstance.i // get
fooInstance.i = 2 // compile-time exception (mutation: reassignment to val)
Объявление параметра в классе case, поскольку var
переопределяет поведение по умолчанию и делает класс case изменчивым:
case class Bar(var i: Int)
val barInstance = Bar(1)
val j = barInstance.i // get
barInstance.i = 2 // set
Другой экземпляр, когда класс case является «изменчивым», - это когда значение в классе case изменено:
import scala.collection._
case class Bar(m: mutable.Map[Int, Int])
val barInstance = Bar(mutable.Map(1 -> 2))
barInstance.m.update(1, 3) // mutate m
barInstance // Bar(Map(1 -> 3)
Обратите внимание, что «мутация», которая происходит здесь, находится на карте, на которую указывает m
, а не на m
. Таким образом, если какой-либо другой объект имеет m
как член, он также увидит изменение. Обратите внимание, как в следующем примере изменение instanceA
также изменяет instanceB
:
import scala.collection.mutable
case class Bar(m: mutable.Map[Int, Int])
val m = mutable.Map(1 ->2)
val barInstanceA = Bar(m)
val barInstanceB = Bar(m)
barInstanceA.m.update(1,3)
barInstanceA // Bar = Bar(Map(1 -> 3))
barInstanceB // Bar = Bar(Map(1 -> 3))
m // scala.collection.mutable.Map[Int,Int] = Map(1 -> 3)
Создание копии объекта с определенными изменениями
Классы классов предоставляют метод copy
который создает новый объект, который имеет те же поля, что и старый, с определенными изменениями.
Мы можем использовать эту функцию для создания нового объекта из предыдущего, который имеет одни и те же характеристики. Этот простой класс case демонстрирует эту функцию:
case class Person(firstName: String, lastName: String, grade: String, subject: String)
val putu = Person("Putu", "Kevin", "A1", "Math")
val mark = putu.copy(firstName = "Ketut", lastName = "Mark")
// mark: People = People(Ketut,Mark,A1,Math)
В этом примере мы видим, что оба объекта имеют схожие характеристики ( grade = A1
, subject = Math
), за исключением тех случаев, когда они были указаны в копии ( firstName
и lastName
).
Классы с одним элементом для безопасности типов
Для достижения безопасности типа иногда мы хотим избежать использования примитивных типов в нашем домене. Например, представьте Person
с name
. Как правило, мы будем кодировать name
как String
. Однако, это не было бы трудно смешивать String
, представляющую Person
«сек name
с String
, представляющим сообщение об ошибке:
def logError(message: ErrorMessage): Unit = ???
case class Person(name: String)
val maybeName: Either[String, String] = ??? // Left is error, Right is name
maybeName.foreach(logError) // But that won't stop me from logging the name as an error!
Чтобы избежать таких ошибок, вы можете кодировать данные следующим образом:
case class PersonName(value: String)
case class ErrorMessage(value: String)
case class Person(name: PersonName)
и теперь наш код не будет компилироваться, если мы смешаем PersonName
с ErrorMessage
или даже обычную String
.
val maybeName: Either[ErrorMessage, PersonName] = ???
maybeName.foreach(reportError) // ERROR: tried to pass PersonName; ErrorMessage expected
maybeName.swap.foreach(reportError) // OK
Но это накладывает небольшие накладные расходы во время выполнения, поскольку теперь нам нужно вставить / удалить String
s в / из своих контейнеров PersonName
. Чтобы этого избежать, можно создавать ErrorMessage
значений PersonName
и ErrorMessage
:
case class PersonName(val value: String) extends AnyVal
case class ErrorMessage(val value: String) extends AnyVal