Как переопределить применить в случае класса компаньон
Итак, вот ситуация. Я хочу определить класс 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 ответов
Причина конфликта в том, что класс 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)
}
Как видите, А больше не case class
, Я не уверен, предназначены ли классы case с неизменяемыми полями для изменения входящих значений, поскольку имя "case case" подразумевает, что должна быть возможность извлечь (неизмененные) аргументы конструктора, используя match
,
ОБНОВЛЕНИЕ 2016/02/25:
Хотя ответ, который я написал ниже, остается достаточным, стоит также сослаться на другой связанный с ним ответ относительно сопутствующего объекта класса дела. А именно, как точно воспроизвести созданный компилятором неявный объект-компаньон, который возникает, когда определяется только сам класс case. Для меня это оказалось нелогичным.
Резюме:
Вы можете изменить значение параметра класса дела до того, как он будет сохранен в классе дела, довольно просто, оставаясь при этом действительным (действующим) ADT (абстрактный тип данных). Несмотря на то, что решение было относительно простым, выяснить детали было немного сложнее.
Подробности:
Если вы хотите, чтобы когда-либо создавались только действительные экземпляры вашего класса case, что является существенным предположением для ADT (абстрактного типа данных), вы должны сделать ряд вещей.
Например, сгенерированный компилятором copy
метод предоставляется по умолчанию для класса case. Таким образом, даже если вы были очень осторожны, чтобы убедиться, что только экземпляры были созданы с помощью явного объекта-компаньона apply
метод, который гарантировал, что они могут содержать только значения в верхнем регистре, следующий код создаст экземпляр класса case со значением в нижнем регистре:
val a1 = A("Hi There") //contains "HI THERE"
val a2 = a1.copy(s = "gotcha") //contains "gotcha"
Кроме того, кейс-классы реализуют java.io.Serializable
, Это означает, что ваша осторожная стратегия, предусматривающая использование только прописных букв, может быть нарушена с помощью простого текстового редактора и десериализации.
Итак, для всех различных способов использования вашего класса дел (доброжелательно и / или злонамеренно), вот действия, которые вы должны предпринять:
- Для вашего явного сопутствующего объекта:
- Создайте его, используя точно то же имя, что и ваш класс дел
- Это имеет доступ к частным частям класса дела
- Создать
apply
метод с точно такой же сигнатурой, что и основной конструктор для вашего класса case- Это будет успешно скомпилировано после выполнения шага 2.1
- Предоставить реализацию, получающую экземпляр класса case с использованием
new
оператор и предоставление пустой реализации{}
- Это теперь создаст экземпляр класса case строго на ваших условиях
- Пустая реализация
{}
должен быть предоставлен, потому что класс case объявленabstract
(см. шаг 2.1)
- Создайте его, используя точно то же имя, что и ваш класс дел
- Для вашего случая класса:
- Объявите это
abstract
- Предотвращает компилятор Scala от генерации
apply
Метод в объекте-компаньоне, вызвавший ошибку компиляции "метод определен дважды..." (шаг 1.2 выше)
- Предотвращает компилятор Scala от генерации
- Пометить основной конструктор как
private[A]
- Первичный конструктор теперь доступен только для самого класса case и его сопутствующего объекта (тот, который мы определили выше на шаге 1.1)
- Создать
readResolve
метод- Предоставьте реализацию, используя метод apply (шаг 1.2 выше)
- Создать
copy
метод- Определите, чтобы он имел точно такую же сигнатуру, что и первичный конструктор класса case
- Для каждого параметра добавьте значение по умолчанию, используя то же имя параметра (например:
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)
}
А вот ваш код после реализации 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 (скрывает класс 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 ( дочерний сайт Stackru). Если вы в конечном итоге просматриваете его, используете или используете мое решение, пожалуйста, оставьте мне свои отзывы, предложения или вопросы, и в разумных пределах я постараюсь ответить в течение дня.
Я не знаю, как переопределить 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 позволяет переопределять и отменять применение по умолчанию. Вы можете получить это поведение, дав -Xsource:2.12
опция для компилятора на Scala 2.11.11+.
Работает с переменными var:
case class A(var s: String) {
// Conversion
s = s.toUpperCase
}
Эта практика, видимо, поощряется в классах case вместо определения другого конструктора. Посмотреть здесь., При копировании объекта вы также сохраняете те же изменения.
Другая идея при сохранении класса case и отсутствии неявных определений или другого конструктора состоит в том, чтобы создать сигнатуру 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.
Начиная с 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.
Если вы застряли в более старой версии 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.final