Итак, вот ситуация. Я хочу определить класс case следующим образом:
case class A(val s: String)
и я хочу определить объект, чтобы при создании экземпляров класса значение 's' всегда было в верхнем регистре, например:
object A {
def apply(s: String) = new A(s.toUpperCase)
}
Однако это не работает, поскольку Scala жалуется, что метод apply (s: String) определен дважды. Я понимаю, что синтаксис класса case автоматически определит его для меня, но разве нет другого способа добиться этого? Я хотел бы придерживаться класса case, так как хочу использовать его для сопоставления с образцом.
scala
pattern-matching
case-class
Джон С
источник
источник
Ответы:
Причина конфликта в том, что класс 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 больше не a
case class
. Я не уверен, что классы case с неизменяемыми полями предназначены для модификации входящих значений, поскольку имя «case class» подразумевает, что должна быть возможность извлекать (немодифицированные) аргументы конструктора с помощьюmatch
.источник
toCharArray
Вызов не нужно, можно было бы написатьs.exists(_.isLower)
.s.forall(_.isUpper)
это легче понять, чем!s.exists(_.isLower)
.s.forall(_isupper)
это легче читать. Я буду использовать это в сочетании с предложением @olle.match
".ОБНОВЛЕНИЕ 2016/02/25:
хотя ответ, который я написал ниже, остается достаточным, стоит также сослаться на другой связанный ответ на этот вопрос, касающийся сопутствующего объекта класса case. А именно, как точно воспроизвести сгенерированный компилятором неявный объект-компаньон, который возникает, когда определяется только сам класс case. Для меня это оказалось нелогичным.
Резюме:
Вы можете довольно просто изменить значение параметра класса case до того, как он будет сохранен в классе case, пока он все еще остается действительным (отмеченным) 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 (доброжелательно и / или злонамеренно), вы должны предпринять следующие действия:
apply
метод с точно такой же сигнатурой, что и основной конструктор для вашего класса дела.new
оператора и предоставляющую пустую реализацию{}
{}
Должна быть предоставлена пустая реализация , потому что класс case объявленabstract
(см. Шаг 2.1)abstract
apply
метод в сопутствующем объекте, который вызывал ошибку компиляции «метод определен дважды ...» (шаг 1.2 выше)private[A]
readResolve
методcopy
методs: String = s
)Вот ваш код, измененный указанными выше действиями:
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 (скрывает класс 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). Если вы в конечном итоге изучите его, воспользуетесь моим решением, оставьте мне отзыв, предложения или вопросы, и в пределах разумного я сделаю все возможное, чтобы ответить в течение дня.
источник
Я не знаю, как переопределить
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 по умолчанию?
источник
Proxy
! Хотя может быть лучшеs.toUpperCase
один раз .toUpperCase
больше одного раза.val self
, нетdef self
. У меня только что в голове С ++.Для людей, читающих это после апреля 2017 года: Начиная с Scala 2.12.2+, Scala по умолчанию позволяет переопределить применение и отмену . Вы можете получить такое поведение, также предоставив
-Xsource:2.12
возможность компилятору Scala 2.11.11+.источник
-Xprint
match
Он работает с переменными var:
case class A(var s: String) { // Conversion s = s.toUpperCase }
Эта практика, по-видимому, поощряется в случае классов вместо определения другого конструктора. Глянь сюда. . При копировании объекта вы также сохраняете те же изменения.
источник
Другая идея при сохранении класса case и без неявных defs или другого конструктора - сделать сигнатуру
apply
немного другой, но с точки зрения пользователя такой же. Где-то я видел неявный трюк, но не могу вспомнить / найти, какой это был неявный аргумент, поэтому выбралBoolean
здесь. Если кто-то может мне помочь и довести дело до конца ...object A { def apply(s: String)(implicit ev: Boolean) = new A(s.toLowerCase) } case class A(s: String)
источник
Я столкнулся с той же проблемой, и это решение мне подходит:
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 }
источник
Начиная с 2020 года на Scala 2.13 описанный выше сценарий переопределения метода apply класса case с той же подписью работает полностью нормально.
case class A(val s: String) object A { def apply(s: String) = new A(s.toUpperCase) }
приведенный выше фрагмент компилируется и отлично работает в Scala 2.13 как в режиме REPL, так и в режиме без REPL.
источник
Я думаю, это работает именно так, как вы этого хотите. Вот моя сессия 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
источник