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

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

case class A(val s: String) 

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

    И вот ваш код после реализации требования (предложенный в ответе @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 (скрывает class case как реализацию и создает окончательный class, который предотвращает деривации):

     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) } 

    Хотя это напрямую отвечает на ваш вопрос, есть еще больше способов расширить этот путь вокруг classов 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: возможно ли переопределить конструктор classа case по умолчанию?

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

    Другая идея, сохраняя class 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 } 

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

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

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

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

    Если вы застряли со старой scala, где вы не можете переопределить по умолчанию или вы не хотите добавлять флаг компилятора, как показано в @ mehmet-emre, и вам нужен class 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.final

    Давайте будем гением компьютера.