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

93

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

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

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

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

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

  • 0
    Посмотрите этот связанный вопрос: stackoverflow.com/q/4635765/156410
  • 16
    Почему это не конструктивно? Моды на этом сайте слишком строги. Это имеет конечное число возможных фактических ответов.
Показать ещё 1 комментарий
Теги:
case-class

4 ответа

48
Лучший ответ

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

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

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

  • 48
    Вы можете создать подкласс класса case. Подкласс также не может быть классом case - это ограничение.
86

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

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

Да, и даже можно переопределить (используя 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.

  • 1
    Спасибо за исчерпывающий ответ. Я думаю, что все исключение «Вы не можете подкласс», вероятно, вряд ли меня поэтапно в ближайшее время.
  • 14
    Вы можете создать подкласс класса case. Подкласс также не может быть классом case - это ограничение.
Показать ещё 3 комментария
11

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

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

  • 3
    Я не думаю, что код клиента должен зависеть от точного значения «равно»; класс должен решить, что для него значит «равный». Автор класса должен быть свободен, чтобы изменить реализацию 'equals' вниз по линии.
  • 8
    @pkaeding Вы можете не иметь клиентского кода, который зависит от какого-либо частного метода. Все, что является публичным, - это договор, на который вы согласились.
Показать ещё 4 комментария
3

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

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

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

Та же проблема решена с классами case.

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 Сравнение образцов

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

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

Если то, что вы делаете, в основном создает новые подклассы, то на самом деле объектно-ориентированное решение разложения имеет верх. Причина в том, что очень просто и очень локально изменить просто создать новый подкласс с помощью метода eval, где, как и в функциональном решении, вам нужно будет вернуться и изменить код внутри метода eval и добавьте к нему новый случай.

С другой стороны, , если то, что вы делаете, будет создавать множество новых методов, но сама иерархия классов будет оставаться относительно стабильной, тогда сравнение шаблонов на самом деле выгодно. Потому что, опять же, каждый новый метод в решении сопоставления шаблонов - это локальное изменение, помещаемое в базовый класс или даже вне иерархии классов. В то время как новый метод, такой как показ в объектно-ориентированной декомпозиции, требует нового приращения - каждый подкласс. Таким образом, будет больше частей, которые вы должны прикоснуться.

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

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

Изображение 6067

Ещё вопросы

Сообщество Overcoder
Наверх
Меню