ОНОВЛЕННЯ 2016/02/25:
Незважаючи на те, що відповідь, яку я написав нижче, залишається достатньою, варто також посилатись на іншу відповідну відповідь на це стосовно супутнього об’єкта класу case. А саме, як можна точно відтворити згенерований компілятором неявний об'єкт-супутник, який виникає, коли визначається лише сам клас case. Для мене це виявилось протилежним інтуїтивним.
Короткий зміст:
Ви можете змінити значення параметра класу справи, перш ніж він буде збережений у класі справи досить просто, поки він все ще залишається дійсним (зміненим) ADT (Абстрактний тип даних). Хоча рішення було відносно простим, виявлення деталей було дещо складнішим.
Детально:
Якщо ви хочете, щоб екземпляри могли бути лише дійсними екземплярами вашого класу випадку, що є важливим припущенням для ADT (Абстрактного типу даних), вам слід зробити ряд речей.
Наприклад, сформований компілятор copy
метод, за замовчуванням надається для класу case. Отже, навіть якщо ви були дуже обережні, щоб лише екземпляри створювались за допомогою apply
методу явного об’єкта-супутника, який гарантував, що вони можуть містити лише великі регістри, наступний код створить екземпляр класу справи з малим значенням:
val a1 = A("Hi There")
val a2 = a1.copy(s = "gotcha")
Крім того, реалізуються класи case java.io.Serializable
. Це означає, що ваша обережна стратегія, щоб мати лише екземпляри верхнього регістру, може бути зруйнована за допомогою простого текстового редактора та десеріалізації.
Отже, для всіх різних способів використання вашого класу case (доброзичливо та / або зловмисно), ось такі дії, які ви повинні вжити:
- Для вашого явного об’єкта-супутника:
- Створіть його, використовуючи абсолютно те саме ім’я, що і ваш клас справи
- Це має доступ до приватних частин класу справи
- Створіть
apply
метод із точно таким самим підписом, що і основний конструктор для вашого класу case
- Це буде успішно скомпільовано після завершення кроку 2.1
- Надайте реалізацію, яка отримує екземпляр класу case за допомогою
new
оператора та забезпечує порожню реалізацію{}
- Тепер це створить інстанцію класу справи суворо на ваших умовах
{}
Потрібно надати порожню реалізацію, оскільки оголошено клас case abstract
(див. Крок 2.1)
- Для вашого класу справ:
- Заявіть це
abstract
- Забороняє компілятору Scala генерувати
apply
метод у об'єкті-супутнику, що спричиняє помилку компіляції "метод визначається двічі ..."
- Позначте основний конструктор як
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)
}
І ось ваш код після реалізації require (запропонованого у відповіді @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). Якщо ви все-таки переглянете його, використаєте чи використаєте моє рішення, будь ласка, залиште мені відгук, пропозиції чи запитання, і в межах розумної причини я зроблю все можливе, щоб відповісти протягом дня.