Каковы недостатки объявления тематических классов Scala?
Если вы пишете код, в котором используется множество красивых и неизменных структур данных, классы падежей кажутся находкой, и вы можете бесплатно получить все перечисленное ниже с одним ключевым словом:
- Все неизменяемые по умолчанию
- Геттеры определяются автоматически
- Достойная реализация toString()
- Соответствует equals() и hashCode()
- Сопутствующий объект с методом unapply() для сопоставления
Но каковы недостатки определения неизменяемой структуры данных в качестве класса наблюдения?
Какие ограничения он накладывает на класс или его клиентов?
Существуют ли ситуации, когда вы предпочитаете не-кейс-класс?
5 ответов
Один большой недостаток: классы case не могут расширять класс case. Это ограничение.
Другие преимущества, которые вы пропустили, перечислены для полноты: совместимая сериализация / десериализация, нет необходимости использовать "новое" ключевое слово для создания.
Я предпочитаю не-case классы для объектов с изменяемым состоянием, частным состоянием или отсутствием состояния (например, большинство одноэлементных компонентов). Кейс-классы для всего остального.
Сначала хорошие моменты:
Все неизменяемые по умолчанию
Да, и даже может быть отменено (используя var
) если тебе это нужно
Геттеры определяются автоматически
Возможно в любом классе, добавив префикс в params val
порядочный toString()
реализация
Да, очень полезно, но выполнимо вручную на любом классе, если это необходимо
уступчивый equals()
а также hashCode()
В сочетании с простым сопоставлением с образцом, это основная причина, по которой люди используют классы дел
Сопутствующий объект с unapply()
метод сопоставления
Также возможно сделать вручную на любом классе с помощью экстракторов
Этот список также должен включать в себя сверхмощный метод копирования, один из лучших вариантов Scala 2.8.
Тогда плохо, есть только несколько реальных ограничений с классами case:
Вы не можете определить apply
в объекте-компаньоне, используя ту же сигнатуру, что и метод, сгенерированный компилятором
Однако на практике это редко является проблемой. Изменение поведения сгенерированного метода apply гарантированно удивит пользователей, и его следует настоятельно не поощрять. Единственное оправдание для этого - проверка входных параметров - задача лучше всего выполняется в основном теле конструктора (что также делает проверку доступной при использовании copy
)
Вы не можете подкласс
Правда, хотя для кейс-класса все еще возможно быть потомком. Одним из распространенных шаблонов является построение иерархии классов признаков, используя классы падежей в качестве конечных узлов дерева.
Стоит также отметить sealed
модификатор. Любой подкласс черты с этим модификатором должен быть объявлен в том же файле. При сопоставлении шаблонов с экземплярами признака компилятор может предупредить вас, если вы не проверили все возможные конкретные подклассы. В сочетании с кейс-классами это может дать вам очень высокий уровень доверия к вашему коду, если он компилируется без предупреждения.
Как подкласс Product, классы case не могут иметь более 22 параметров
Никакого реального обходного пути, кроме как прекратить злоупотреблять классами с таким количеством параметров:)
Также...
Еще одно ограничение, которое иногда отмечается, заключается в том, что Scala (в настоящее время) не поддерживает ленивые параметры (например, lazy val
с, но в качестве параметров). Обходной путь к этому состоит в том, чтобы использовать параметр по имени и назначить его ленивому значению в конструкторе. К сожалению, параметры по именам не смешиваются с сопоставлением с образцом, что не позволяет использовать технику с классами case, поскольку это нарушает сгенерированный компилятором экстрактор.
Это актуально, если вы хотите реализовать высокофункциональные ленивые структуры данных, и, надеюсь, будет решено с добавлением ленивых параметров в будущий выпуск Scala.
Я думаю, что принцип TDD применим здесь: не переусердствуйте. Когда вы объявляете что-то case class
Вы заявляете много функциональности. Это уменьшит гибкость, которую вы имеете в изменении класса в будущем.
Например, case class
имеет equals
метод над параметрами конструктора. Возможно, вас это не волнует, когда вы впервые пишете свой класс, но, во-вторых, вы можете решить, что хотите, чтобы равенство игнорировало некоторые из этих параметров, или сделайте что-то немного другое. Однако клиентский код может быть написан в то же время, что зависит от case class
равенство.
Существуют ли ситуации, когда вы предпочитаете не-кейс-класс?
Мартин Одерски дает нам хорошую отправную точку в своем курсе " Принципы функционального программирования в Scala" (лекция 4.6 - Сопоставление с образцом), который мы могли бы использовать, когда нам нужно выбрать между классом и классом прецедентов. Глава 7 Scala By Example содержит тот же пример.
Скажем, мы хотим написать интерпретатор для арифметических выражений. Для простоты сначала мы ограничимся только числами и + операциями. Такие выражения могут быть представлены в виде иерархии классов с абстрактным базовым классом Expr в качестве корня и двумя подклассами Number и Sum. Тогда выражение 1 + (3 + 7) будет представлено как
новая сумма (новый номер (1), новая сумма (новый номер (3), новый номер (7)))
abstract class Expr {
def eval: Int
}
class Number(n: Int) extends Expr {
def eval: Int = n
}
class Sum(e1: Expr, e2: Expr) extends Expr {
def eval: Int = e1.eval + e2.eval
}
Кроме того, добавление нового класса Prod не влечет за собой никаких изменений в существующем коде:
class Prod(e1: Expr, e2: Expr) extends Expr {
def eval: Int = e1.eval * e2.eval
}
Напротив, добавление нового метода требует модификации всех существующих классов.
abstract class Expr {
def eval: Int
def print
}
class Number(n: Int) extends Expr {
def eval: Int = n
def print { Console.print(n) }
}
class Sum(e1: Expr, e2: Expr) extends Expr {
def eval: Int = e1.eval + e2.eval
def print {
Console.print("(")
print(e1)
Console.print("+")
print(e2)
Console.print(")")
}
}
Та же проблема решена с кейс-классами.
abstract class Expr {
def eval: Int = this match {
case Number(n) => n
case Sum(e1, e2) => e1.eval + e2.eval
}
}
case class Number(n: Int) extends Expr
case class Sum(e1: Expr, e2: Expr) extends Expr
Добавление нового метода является локальным изменением.
abstract class Expr {
def eval: Int = this match {
case Number(n) => n
case Sum(e1, e2) => e1.eval + e2.eval
}
def print = this match {
case Number(n) => Console.print(n)
case Sum(e1,e2) => {
Console.print("(")
print(e1)
Console.print("+")
print(e2)
Console.print(")")
}
}
}
Добавление нового класса Prod потенциально может изменить все сопоставления с образцом.
abstract class Expr {
def eval: Int = this match {
case Number(n) => n
case Sum(e1, e2) => e1.eval + e2.eval
case Prod(e1,e2) => e1.eval * e2.eval
}
def print = this match {
case Number(n) => Console.print(n)
case Sum(e1,e2) => {
Console.print("(")
print(e1)
Console.print("+")
print(e2)
Console.print(")")
}
case Prod(e1,e2) => ...
}
}
Стенограмма из видеолектуры 4.6 Pattern Matching
Оба эти дизайна прекрасно подходят, и выбор между ними иногда зависит от стиля, но, тем не менее, есть некоторые критерии, которые важны.
Одним из критериев может быть, вы чаще создаете новые подклассы выражения или вы чаще создаете новые методы? Так что это критерий, который учитывает будущую расширяемость и возможный этап расширения вашей системы.
Если то, что вы делаете, в основном создает новые подклассы, то на самом деле решение объектно-ориентированной декомпозиции имеет преимущество. Причина в том, что очень просто и очень локально изменить просто создать новый подкласс с методом eval, где, как и в функциональном решении, вам придется вернуться назад и изменить код внутри метода eval и добавить новый случай к этому.
С другой стороны, если то, что вы делаете, будет создавать много новых методов, но сама иерархия классов будет оставаться относительно стабильной, то сопоставление с образцом на самом деле выгодно. Потому что, опять же, каждый новый метод в решении сопоставления с образцом является просто локальным изменением, независимо от того, помещаете ли вы его в базовый класс или, возможно, даже вне иерархии классов. В то время как новый метод, такой как show в объектно-ориентированной декомпозиции, потребует нового приращения каждого подкласса. Так что было бы больше деталей, К которым вы должны прикоснуться.
Таким образом, проблема этой расширяемости в двух измерениях, когда вы можете захотеть добавить новые классы в иерархию, или вы можете захотеть добавить новые методы, или, возможно, оба, была названа проблемой выражения.
Помните: мы должны использовать это как отправную точку, а не как единственный критерий.
Я цитирую это из Scala cookbook
от Alvin Alexander
Глава 6: objects
,
Это одна из многих вещей, которые мне показались интересными в этой книге.
Чтобы обеспечить несколько конструкторов для класса case, важно знать, что фактически делает объявление класса case.
case class Person (var name: String)
Если вы посмотрите на код, сгенерированный компилятором Scala для примера класса case, вы увидите, что он создает два выходных файла: Person $.class и Person.class. Если вы разберете Person $.class с помощью команды javap, вы увидите, что она содержит метод apply, наряду со многими другими:
$ javap Person$
Compiled from "Person.scala"
public final class Person$ extends scala.runtime.AbstractFunction1 implements scala.ScalaObject,scala.Serializable{
public static final Person$ MODULE$;
public static {};
public final java.lang.String toString();
public scala.Option unapply(Person);
public Person apply(java.lang.String); // the apply method (returns a Person) public java.lang.Object readResolve();
public java.lang.Object apply(java.lang.Object);
}
Вы также можете разобрать Person.class, чтобы увидеть, что он содержит. Для такого простого класса он содержит еще 20 методов; это скрытое раздувание - одна из причин, по которой некоторым разработчикам не нравятся классы дел.