Как переопределить применить в случае вспомогательный класс


Так вот в чем дело. Я хочу определить класс 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 73

9 ответов:

причина конфликта заключается в том, что класс 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)
}

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

обновление 2016/02/25:
Хотя ответ, который я написал ниже, остается достаточным, стоит также сослаться на другой связанный с этим ответ относительно сопутствующего объекта класса case. А именно, как точно воспроизвести созданный компилятором неявный сопутствующий объект что происходит, когда человек определяет только сам класс. Для меня это оказалось интуитивно.


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

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

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

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

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

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

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

и вот ваш код после реализации require (предлагается в @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 (сестринский сайт StackOverflow). Если вы в конечном итоге просматриваете его, используя или используя мое решение, Пожалуйста, подумайте о том, чтобы оставить мне отзывы, предложения или вопросы, и в разумных пределах я сделаю все возможное, чтобы ответить в течение дня.

Я не знаю как переопределить 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 по умолчанию?

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

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

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

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

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

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

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

sealed trait A {
  def s:String
}

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

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

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

case class A(private val _s: String) {
  val s = _s.toUpperCase
}

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