Подтвердить что ты не робот

Как переопределить применение в компаньоне класса case

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

case class A(val s: String)

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

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

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

4b9b3361

Ответ 1

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

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

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

Ответ 2

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


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

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

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

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

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

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

  • Для вашего явного сопутствующего объекта:
    • Создайте его, используя то же имя, что и ваш класс case
      • Доступ к частным частям класса case
    • Создайте метод apply с точно такой же подписью, что и основной конструктор для вашего класса case
      • Это будет успешно скомпилировано после завершения этапа 2.1.
    • Предоставьте реализацию, получающую экземпляр класса case с помощью оператора new и предоставляющий пустую реализацию {}
      • Теперь это будет экземпляр класса case строго на ваших условиях
      • Пустая реализация {} должна быть предоставлена, поскольку класс case объявлен abstract (см. шаг 2.1)
  • Для вашего класса case:
    • Объявить его abstract
      • Предотвращает компилятор Scala от создания метода apply в сопутствующем объекте, что является причиной ошибки компиляции "метод определяется дважды..." (шаг 1.2 выше)
    • Отметьте основной конструктор как private[A]
      • Основной конструктор теперь доступен только для класса case и его сопутствующего объекта (тот, который мы определили выше на шаге 1.1)
    • Создайте метод readResolve
      • Предоставить реализацию с использованием метода apply (шаг 1.2 выше)
    • Создайте метод copy
      • Определите, что он имеет точно такую ​​же подпись, что и первичный конструктор класса case
      • Для каждого параметра добавьте значение по умолчанию с использованием того же имени параметра (ex: s: String = s)
      • Предоставить реализацию с использованием метода apply (шаг 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)
}

И вот ваш код после реализации запроса (предложенный в ответе @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 interop (скрывает класс 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)
}

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

Ответ 3

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

class UpperCaseString(s: String) extends Proxy {
  val self: String = s.toUpperCase
}

implicit def stringToUpperCaseString(s: String) = new UpperCaseString(s)
implicit def upperCaseStringToString(s: UpperCaseString) = s.self

case class A(val s: UpperCaseString)

println(A("hello"))

Вышеупомянутый код выводит:

A(HELLO)

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

Ответ 4

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

object A {
  def apply(s: String)(implicit ev: Boolean) = new A(s.toLowerCase)
}
case class A(s: String)

Ответ 5

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

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

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

Ответ 6

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

sealed trait A {
  def s:String
}

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

И, если какой-либо метод необходим, просто определите его в свойстве и переопределите его в классе case.

Ответ 7

Я думаю, что это работает именно так, как вы этого хотите. Здесь мой сеанс 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