Как переопределить применить в случае класса компаньон - PullRequest
80 голосов
/ 29 апреля 2011

Итак, вот ситуация.Я хочу определить класс case следующим образом:

case class A(val s: String)

, и я хочу определить объект, чтобы при создании экземпляров класса значение для s всегда было в верхнем регистре, например

object A {
  def apply(s: String) = new A(s.toUpperCase)
}

Однако это не работает, поскольку Scala жалуется, что метод apply (s: String) определен дважды.Я понимаю, что синтаксис класса case автоматически определит его для меня, но разве нет другого способа добиться этого?Я хотел бы придерживаться класса case, поскольку хочу использовать его для сопоставления с образцом.

Ответы [ 9 ]

86 голосов
/ 29 апреля 2011

Причина конфликта заключается в том, что класс case предоставляет точно такой же метод apply () (с той же подписью).

Прежде всего я хотел бы предложить вам использовать require:

case class A(s: String) {
  require(! s.toCharArray.exists( _.isLower ), "Bad string: "+ s)
}

Это вызовет исключение, если пользователь попытается создать экземпляр, в котором s включает символы нижнего регистра.Это хорошее использование классов case, так как то, что вы помещаете в конструктор, это то, что вы получаете, когда используете сопоставление с образцом (match).

Если это не то, что вы хотите, то я бысделать конструктор private и заставить пользователей только использовать метод apply:

class A private (val s: String) {
}

object A {
  def apply(s: String): A = new A(s.toUpperCase)
}

Как видите, A больше не является case class.Я не уверен, предназначены ли классы case с неизменяемыми полями для изменения входящих значений, поскольку имя «case case» подразумевает, что должна быть возможность извлечь (неизмененные) аргументы конструктора, используя match.

25 голосов
/ 28 августа 2014

ОБНОВЛЕНИЕ 2016/02/25:
Несмотря на то, что ответ, который я написал ниже, остается достаточным, стоит также сослаться на другой связанный с ним ответ относительно сопутствующего объекта класса случая.А именно, как точно воспроизвести сгенерированный компилятором неявный объект-компаньон , который возникает, когда определяется только сам класс case.Для меня это оказалось противоинтуитивно.


Сводка:
Вы можете изменить значение параметра класса дела перед его сохранением в классе дела довольнопросто пока он все еще остается действительным ADT (абстрактный тип данных).В то время как решение было относительно простым, выяснить подробности было немного сложнее.

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

Например, сгенерированный компилятором метод copy предоставляется по умолчанию для класса case.Таким образом, даже если вы очень внимательно следите за тем, чтобы с помощью метода apply явного сопутствующего объекта создавались только экземпляры, которые гарантировали, что они могут содержать только значения верхнего регистра, следующий код создаст экземпляр класса case со значением нижнего регистра:1021 *

val a1 = A("Hi There") //contains "HI THERE"
val a2 = a1.copy(s = "gotcha") //contains "gotcha"

Кроме того, классы case реализуют java.io.Serializable.Это означает, что ваша осторожная стратегия, предусматривающая использование только прописных букв, может быть нарушена с помощью простого текстового редактора и десериализации.

Итак, для всех различных способов использования вашего класса case (доброжелательно и / или злонамеренно), вот действия, которые вы должны предпринять:

  1. Для вашего явного сопутствующего объекта:
    1. Создайте его, используя точно такое же имя, как у вашего класса дел
      • Имеется доступ к закрытым частям класса дел
    2. Создайте метод applyс точно такой же сигнатурой, что и у основного конструктора для вашего case-класса
      • Он будет успешно скомпилирован после выполнения шага 2.1
    3. Обеспечение реализации, получающей экземпляр класса caseиспользуя оператор new и предоставляя пустую реализацию {}
      • Теперь это будет создавать экземпляр класса case строго на ваших условиях
      • Должна быть предоставлена ​​пустая реализация {}, поскольку класс caseобъявляется abstract (см. шаг 2.1)
  2. Для класса вашего дела:
    1. Объявите его abstract
      • ПредотвращатьКомпилятор Scala генерирует метод apply в сопутствующем объекте, что и является причиной ошибки компиляции «метод определен дважды ...» (шаг 1.2 выше)
    2. Отметитьпервичный конструктор как private[A]
      • Первичный конструктор теперь доступен только для самого класса case и его сопутствующего объекта (тот, который мы определили выше на шаге 1.1)
    3. Создание readResolve метода
      1. Предоставление реализации с использованием метода apply (шаг 1.2 выше)
    4. Создание copy метода
      1. Определите, чтобы он имел точно такую ​​же сигнатуру, что и основной конструктор класса наблюдения
      2. Для каждого параметра добавьте значение по умолчанию, используя то же имя параметра (например: s: String = s)
      3. Обеспечьте реализацию, используяметод применения (шаг 1.2 ниже)

Вот ваш код, измененный с помощью вышеуказанных действий:

object A {
  def apply(s: String, i: Int): A =
    new A(s.toUpperCase, i) {} //abstract class implementation intentionally empty
}
abstract case class A private[A] (s: String, i: Int) {
  private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
    A.apply(s, i)
  def copy(s: String = s, i: Int = i): A =
    A.apply(s, i)
}

А вот вашкод после реализации требованияuire (предложено в ответе @ollekullberg), а также определение идеального места для размещения любого вида кэширования:

object A {
  def apply(s: String, i: Int): A = {
    require(s.forall(_.isUpper), s"Bad String: $s")
    //TODO: Insert normal instance caching mechanism here
    new A(s, i) {} //abstract class implementation intentionally empty
  }
}
abstract case class A private[A] (s: String, i: Int) {
  private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
    A.apply(s, i)
  def copy(s: String = s, i: Int = i): A =
    A.apply(s, i)
}

И эта версия более безопасна / надежна, если этот код будет использоваться через взаимодействие Java (скрываетсякласс case как реализация и создает конечный класс, который предотвращает деривации):

object A {
  private[A] abstract case class AImpl private[A] (s: String, i: Int)
  def apply(s: String, i: Int): A = {
    require(s.forall(_.isUpper), s"Bad String: $s")
    //TODO: Insert normal instance caching mechanism here
    new A(s, i)
  }
}
final class A private[A] (s: String, i: Int) extends A.AImpl(s, i) {
  private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
    A.apply(s, i)
  def copy(s: String = s, i: Int = i): A =
    A.apply(s, i)
}

Хотя это прямо отвечает на ваш вопрос, есть еще больше способов расширить этот путь вокруг классов дел, помимо кэширования экземпляров. Для собственных нужд проекта я создал еще более обширное решение , которое я задокументировал на CodeReview (дочерний сайт StackOverflow). Если вы в конечном итоге просматриваете его, используете или используете мое решение, пожалуйста, оставьте мне свои отзывы, предложения или вопросы, и в разумных пределах я сделаю все возможное, чтобы ответить в течение дня.

12 голосов
/ 29 апреля 2011

Я не знаю, как переопределить метод apply в сопутствующем объекте (если это даже возможно), но вы также можете использовать специальный тип для строк в верхнем регистре:приведенные выше выходные данные кода:

A(HELLO)

Вам также следует взглянуть на этот вопрос и ответы на него: Scala: возможно ли переопределить конструктор класса case по умолчанию?

6 голосов
/ 05 февраля 2018

Для людей, читающих это после апреля 2017 года: Начиная с Scala 2.12.2+, Scala позволяет отменять применение и отмену по умолчанию . Вы можете получить такое поведение, задав опцию -Xsource:2.12 компилятору также в Scala 2.11.11+.

4 голосов
/ 28 ноября 2013

Работает с переменными var:

case class A(var s: String) {
   // Conversion
   s = s.toUpperCase
}

Эта практика, видимо, рекомендуется в классах case вместо определения другого конструктора. См. Здесь. . При копировании объекта вы также сохраняете те же изменения.

4 голосов
/ 29 апреля 2011

Другая идея, сохраняя класс case и не имея неявных определений или другого конструктора, заключается в том, чтобы сделать сигнатуру apply немного другой, но с точки зрения пользователя, такой же. Где-то я видел неявную уловку, но не могу вспомнить / найти, какой это был неявный аргумент, поэтому я выбрал Boolean здесь. Если кто-то может мне помочь и закончить трюк ...

object A {
  def apply(s: String)(implicit ev: Boolean) = new A(s.toLowerCase)
}
case class A(s: String)
2 голосов
/ 21 января 2016

Я столкнулся с той же проблемой, и это решение мне подходит:

sealed trait A {
  def s:String
}

object A {
  private case class AImpl(s:String)
  def apply(s:String):A = AImpl(s.toUpperCase)
}

И, если нужен какой-либо метод, просто определите его в признаке и переопределите в классе случая.

0 голосов
/ 25 июня 2018

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

case class A(private val _s: String) {
  val s = _s.toUpperCase
}
0 голосов
/ 29 апреля 2011

Я думаю, что это работает именно так, как вы этого хотите. Вот моя сессия REPL:

scala> case class A(val s: String)
defined class A

scala> object A {
     | def apply(s: String) = new A(s.toUpperCase)
     | }
defined module A

scala> A("hello")
res0: A = A(HELLO)

Используется Scala 2.8.1.final

...