UPDATE 2016/02/25:
Chociaż odpowiedź, którą napisałem poniżej, pozostaje wystarczająca, warto również odwołać się do innej powiązanej odpowiedzi na to, dotyczącej obiektu towarzyszącego klasy przypadku. Mianowicie, w jaki sposób dokładnie odtworzyć niejawny obiekt towarzyszący wygenerowany przez kompilator, który pojawia się, gdy definiuje się tylko samą klasę przypadku. Dla mnie okazało się to sprzeczne z intuicją.
Podsumowanie:
Możesz zmienić wartość parametru klasy przypadku, zanim zostanie on zapisany w klasie przypadku, w prosty sposób, podczas gdy nadal pozostanie prawidłowym (ated) ADT (Abstract Data Type). Chociaż rozwiązanie było stosunkowo proste, odkrycie szczegółów było nieco trudniejsze.
Szczegóły:
Jeśli chcesz mieć pewność, że tylko poprawne instancje klasy przypadku będą mogły zostać kiedykolwiek utworzone, co jest podstawowym założeniem ADT (abstrakcyjny typ danych), musisz zrobić kilka rzeczy.
Na przykład copy
metoda wygenerowana przez kompilator jest domyślnie udostępniana w klasie sprawy. Tak więc, nawet gdybyś był bardzo ostrożny, aby upewnić się, że tylko instancje zostały utworzone za pomocą metody jawnego obiektu towarzyszącego, apply
która gwarantuje, że mogą one zawsze zawierać tylko wielkie litery, poniższy kod utworzy instancję klasy z małymi literami:
val a1 = A("Hi There")
val a2 = a1.copy(s = "gotcha")
Dodatkowo klasy przypadków implementują java.io.Serializable
. Oznacza to, że twoją ostrożną strategię, aby mieć tylko wielkie litery, można obalić za pomocą prostego edytora tekstu i deserializacji.
Tak więc, dla wszystkich różnych sposobów wykorzystania klasy sprawy (życzliwie i / lub wrogo), oto działania, które musisz wykonać:
- W przypadku obiektu towarzyszącego:
- Utwórz go, używając dokładnie tej samej nazwy, co klasa sprawy
- Ma to dostęp do części intymnych klasy sprawy
- Utwórz
apply
metodę z dokładnie tym samym podpisem, co główny konstruktor dla Twojej klasy case
- Kompilacja zakończy się powodzeniem po zakończeniu kroku 2.1
- Podaj implementację uzyskując wystąpienie klasy case przy użyciu
new
operatora i podając pustą implementację{}
- Spowoduje to teraz utworzenie instancji klasy przypadku ściśle według Twoich warunków
{}
Należy podać pustą implementację, ponieważ została zadeklarowana klasa przypadku abstract
(patrz krok 2.1)
- Dla Twojej klasy sprawy:
- Zadeklaruj to
abstract
- Zapobiega generowaniu przez kompilator Scali
apply
metody w obiekcie towarzyszącym, co powoduje błąd kompilacji „metoda jest zdefiniowana dwukrotnie ...” (krok 1.2 powyżej)
- Oznacz głównego konstruktora jako
private[A]
- Główny konstruktor jest teraz dostępny tylko dla samej klasy przypadku i jej obiektu towarzyszącego (tego, który zdefiniowaliśmy powyżej w kroku 1.1)
- Utwórz
readResolve
metodę
- Zapewnij implementację przy użyciu metody Apply (krok 1.2 powyżej)
- Utwórz
copy
metodę
- Zdefiniuj go tak, aby miał dokładnie taką samą sygnaturę jak główny konstruktor klasy przypadku
- Dla każdego parametru, należy dodać wartość domyślną przy użyciu tej samej nazwy parametru (np
s: String = s
)
- Zapewnij implementację za pomocą metody zastosuj (krok 1.2 poniżej)
Oto twój kod zmodyfikowany za pomocą powyższych działań:
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)
}
A oto twój kod po zaimplementowaniu wymagania (sugerowanego w odpowiedzi @ollekullberg), a także wskazaniu idealnego miejsca do umieszczenia dowolnego rodzaju buforowania:
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)
}
Ta wersja jest bezpieczniejsza / bardziej niezawodna, jeśli ten kod zostanie użyty za pośrednictwem środowiska Java (ukrywa klasę przypadku jako implementację i tworzy ostateczną klasę, która zapobiega wyprowadzaniu):
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)
}
Chociaż to bezpośrednio odpowiada na twoje pytanie, istnieje jeszcze więcej sposobów na rozszerzenie tej ścieżki o klasy przypadków poza buforowanie instancji. Na potrzeby własnego projektu stworzyłem jeszcze bardziej rozbudowane rozwiązanie, które udokumentowałem w witrynie CodeReview (siostrzanej witrynie StackOverflow). Jeśli w końcu przejrzysz moje rozwiązanie, użyjesz lub wykorzystasz moje rozwiązanie, rozważ zostawienie mi opinii, sugestii lub pytań. Postaram się odpowiedzieć w ciągu jednego dnia.