Каковы недостатки объявления классов классов Scala?

Если вы пишете код, который использует множество красивых, неизменных структур данных, классы case выглядят как находка, давая вам все следующие бесплатно только с одним ключевым словом:

  • Все неизменное по умолчанию
  • Получатели автоматически определены
  • Достойная реализация toString()
  • Соответствие equals() и hashCode()
  • Объект Companion с методом unapply() для сопоставления

Но каковы недостатки определения неизменяемой структуры данных как класса case?

Какие ограничения он устанавливает для класса или его клиентов?

Есть ли ситуации, когда вы предпочитаете класс, не относящийся к какому-либо случаю?

Ответ 1

Один большой недостаток: классы case не могут распространять класс case. Это ограничение.

Другие преимущества, которые вы пропустили, перечислены для полноты: совместимая сериализация/десериализация, нет необходимости использовать "новое" ключевое слово для создания.

Я предпочитаю не-case-классы для объектов с изменяемым состоянием, частным состоянием или отсутствием состояния (например, для большинства одноэлементных компонентов). Примеры классов для почти всего остального.

Ответ 2

Сначала хорошие бит:

Все неизменяемые по умолчанию

Да, и даже можно переопределить (используя var), если вам это нужно

Получатели автоматически определяются

Возможно в любом классе с помощью префикса params с помощью val

Приличная toString() реализация

Да, очень полезно, но выполнимо вручную в любом классе, если необходимо

Соответствие equals() и hashCode()

В сочетании с простым сопоставлением шаблонов это основная причина, по которой люди используют классы case

Объект Companion с unapply() методом сопоставления

Также можно сделать вручную на любом классе с помощью экстракторов

В этот список также должен быть включен uber-мощный метод копирования, один из лучших способов прийти к Scala 2.8


Тогда плохое, есть только несколько реальных ограничений с классами case:

Вы не можете определить apply в сопутствующем объекте, используя ту же подпись, что и метод, созданный компилятором

На практике это редко бывает проблемой. Изменение поведения сгенерированного метода приложения гарантированно удивит пользователей и должно быть сильно обескуражено, единственное оправдание для этого - проверить входные параметры - задачу, которая лучше всего выполняется в основном корпусе конструктора (что также делает проверку доступной при использовании copy)

Вы не можете подклассы

Правда, хотя для класса case все же возможно быть потомком. Один общий шаблон состоит в том, чтобы создать иерархию признаков класса, используя классы case как листовые узлы дерева.

Также стоит отметить модификатор sealed. Любой подкласс признака с этим модификатором должен быть объявлен в том же файле. При сопоставлении шаблонов с экземплярами признака компилятор может затем предупредить вас, если вы не проверили все возможные конкретные подклассы. В сочетании с классами классов это может обеспечить вам очень высокий уровень уверенности в вашем коде, если он компилируется без предупреждения.

В качестве подкласса Product классы case не могут иметь более 22 параметров

Нет реального обходного пути, кроме как прекратить злоупотреблять классами с помощью этого множества параметров:)

Также...

Еще одно ограничение, которое иногда отмечалось, заключается в том, что Scala не поддерживает (в настоящее время) поддержку ленивых параметров (например, lazy val s, а как параметры). Обходной путь к этому - использовать параметр by-name и назначить его ленивому val в конструкторе. К сожалению, параметры by-name не смешиваются с совпадением шаблонов, что предотвращает использование метода с классами case, поскольку он разбивает созданный компилятором экстрактор.

Это актуально, если вы хотите реализовать высокофункциональные ленивые структуры данных и, будем надеяться, будут разрешены с добавлением ленивых параметров в будущую версию Scala.

Ответ 3

Я думаю, что принцип TDD применяется здесь: не перепроектируйте. Когда вы объявляете что-то вроде case class, вы объявляете много функций. Это уменьшит гибкость при изменении класса в будущем.

Например, case class имеет метод equals по параметрам конструктора. Возможно, вам это не понравится, когда вы впервые напишите свой класс, но, наконец, можете решить, хотите ли вы, чтобы равенство игнорировало некоторые из этих параметров или что-то другое. Однако клиентский код может быть записан в среднем времени, которое зависит от равенства case class.

Ответ 4

Существуют ли ситуации, когда вы предпочитаете не-кейс-класс?

Мартин Одерски дает нам хорошую отправную точку в своем курсе " Принципы функционального программирования в Scala" (лекция 4.6 - Сопоставление с образцом), который мы могли бы использовать, когда нам нужно выбрать между классом и классом прецедентов. Глава 7 Scala By Example содержит тот же пример.

Скажем, мы хотим написать интерпретатор для арифметических выражений. Для простоты сначала мы ограничимся только числами и + операциями. Такие expres- -ионы могут быть представлены в виде иерархии классов с абстрактным базовым классом 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 в объектно-ориентированной декомпозиции, потребует нового приращения каждого подкласса. Так что было бы больше деталей, К которым вы должны прикоснуться.

Таким образом, проблема этой расширяемости в двух измерениях, где вы можете захотеть добавить новые классы в иерархию, или вы можете захотеть добавить новые методы, или, возможно, оба, была названа проблемой выражения.

Помните: мы должны использовать это как отправную точку, а не как единственный критерий.

enter image description here

Ответ 5

Я цитирую это из 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 методов; это скрытое раздувание - одна из причин, по которой некоторым разработчикам не нравятся тематические классы.