ОБНОВЛЕНИЕ 2016/02/25:
хотя ответ, который я написал ниже, остается достаточным, стоит также сослаться на другой связанный ответ на этот вопрос, касающийся сопутствующего объекта класса case. А именно, как точно воспроизвести сгенерированный компилятором неявный объект-компаньон, который возникает, когда определяется только сам класс case. Для меня это оказалось нелогичным.
Резюме:
Вы можете довольно просто изменить значение параметра класса case до того, как он будет сохранен в классе case, пока он все еще остается действительным (отмеченным) ADT (абстрактный тип данных). Хотя решение было относительно простым, раскрыть детали было немного сложнее.
Подробности:
если вы хотите гарантировать, что только допустимые экземпляры вашего класса case могут когда-либо быть созданы, что является важным предположением, лежащим в основе ADT (абстрактного типа данных), вы должны сделать ряд вещей.
Например, сгенерированный компилятором copy
метод предоставляется по умолчанию для класса case. Таким образом, даже если вы очень внимательно следите за тем, чтобы с помощью явного apply
метода сопутствующего объекта были созданы только экземпляры, который гарантировал, что они могут содержать только значения в верхнем регистре, следующий код создаст экземпляр класса case со значением нижнего регистра:
val a1 = A("Hi There")
val a2 = a1.copy(s = "gotcha")
Кроме того, классы case реализуют java.io.Serializable
. Это означает, что ваша осторожная стратегия использования экземпляров только в верхнем регистре может быть нарушена с помощью простого текстового редактора и десериализации.
Итак, для всех различных способов использования вашего класса case (доброжелательно и / или злонамеренно), вы должны предпринять следующие действия:
- Для вашего явного объекта-компаньона:
- Создайте его, используя то же имя, что и ваш класс case
- У него есть доступ к частным частям класса case
- Создайте
apply
метод с точно такой же сигнатурой, что и основной конструктор для вашего класса дела.
- Это будет успешно скомпилировано после завершения шага 2.1.
- Предоставьте реализацию, получающую экземпляр класса case с помощью
new
оператора и предоставляющую пустую реализацию{}
- Теперь это создаст экземпляр класса case строго на ваших условиях.
{}
Должна быть предоставлена пустая реализация , потому что класс case объявлен abstract
(см. Шаг 2.1)
- Для вашего класса дела:
- Объявить это
abstract
- Запрещает компилятору Scala генерировать
apply
метод в сопутствующем объекте, который вызывал ошибку компиляции «метод определен дважды ...» (шаг 1.2 выше)
- Отметьте основной конструктор как
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 case class A private[A] (s: String, i: Int) {
private def readResolve(): Object =
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")
new A(s, i) {}
}
}
abstract case class A private[A] (s: String, i: Int) {
private def readResolve(): Object =
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")
new A(s, i)
}
}
final class A private[A] (s: String, i: Int) extends A.AImpl(s, i) {
private def readResolve(): Object =
A.apply(s, i)
def copy(s: String = s, i: Int = i): A =
A.apply(s, i)
}
Хотя это прямо отвечает на ваш вопрос, есть еще больше способов расширить этот путь вокруг классов случаев, помимо кеширования экземпляров. Для нужд моего собственного проекта я создал еще более обширное решение, которое я задокументировал на CodeReview (родственный сайт StackOverflow). Если вы в конечном итоге изучите его, воспользуетесь моим решением, оставьте мне отзыв, предложения или вопросы, и в пределах разумного я сделаю все возможное, чтобы ответить в течение дня.