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


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

  • все неизменное по умолчанию
  • геттеры определяются автоматически
  • достойная реализация toString ()
  • совместимый equals () и hashCode ()
  • сопутствующий объект с методом unapply () для сопоставления

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

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

есть ли ситуации, когда вы должны предпочесть не-case класс?

5 101

5 ответов:

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

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

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

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

все неизменное по умолчанию

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

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

возможно в любом классе путем префикса params с val

приличный toString() реализация

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

совместимость equals() и hashCode()

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

сопутствующий объект с unapply() метод сопоставления

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

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


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

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

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

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

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

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

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

нет реального обходного пути, кроме как прекратить злоупотребление классами при этом многие параметры :)

также...

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

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

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

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

есть ли ситуации, когда вы должны предпочесть не-case класс?

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

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

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

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

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

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

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

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

enter image description here

я цитирую это из Scala cookbook by Alvin Alexander Глава 6.objects.

Это одна из многих вещей, которые я нашел интересным в этой книге.

чтобы предоставить несколько конструкторов для класса case, важно знать, что на самом деле делает объявление класса case.

case class Person (var name: String)

если вы посмотрите на код, который компилятор Scala генерирует для примера класса case, вы увидите, что он создает два выходных файла Person$.класс и личность.класс. Если вы разбирать человека.класс с командой 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);
    }

вы также можете разобрать человека.класса, чтобы увидеть, что он содержит. Для такого простого класса он содержит дополнительные 20 методов; это скрытое раздувание является одной из причин, по которой некоторые разработчики не любят классы case.