UPDATE 25.02.2016:
Obwohl die Antwort, die ich unten geschrieben habe, weiterhin ausreichend ist, lohnt es sich, auch auf eine andere verwandte Antwort in Bezug auf das Begleitobjekt der Fallklasse zu verweisen. Das heißt, wie kann man genau den Compiler erzeugte implizite Begleitobjekt reproduzieren , das auftritt , wenn man nur den Fall der Klasse selbst definiert. Für mich erwies es sich als kontraintuitiv.
Zusammenfassung:
Sie können den Wert eines Fallklassenparameters ändern, bevor er in der Fallklasse gespeichert wird, ganz einfach, während er noch ein gültiger (ated) ADT (Abstract Data Type) bleibt. Während die Lösung relativ einfach war, war es etwas schwieriger, die Details zu entdecken.
Details:
Wenn Sie sicherstellen möchten, dass nur gültige Instanzen Ihrer Fallklasse instanziiert werden können, was eine wesentliche Voraussetzung für einen ADT (Abstract Data Type) ist, müssen Sie eine Reihe von Maßnahmen ergreifen.
Beispielsweise copy
wird standardmäßig eine vom Compiler generierte Methode für eine Fallklasse bereitgestellt. Selbst wenn Sie sehr sorgfältig darauf achten würden, dass nur Instanzen über die apply
Methode des expliziten Begleitobjekts erstellt werden, die garantiert, dass sie immer nur Großbuchstaben enthalten können, würde der folgende Code eine Fallklasseninstanz mit einem Kleinbuchstabenwert erzeugen:
val a1 = A("Hi There") //contains "HI THERE"
val a2 = a1.copy(s = "gotcha") //contains "gotcha"
Zusätzlich werden Fallklassen implementiert java.io.Serializable
. Dies bedeutet, dass Ihre sorgfältige Strategie, nur Großbuchstaben zu verwenden, mit einem einfachen Texteditor und einer Deserialisierung unterlaufen werden kann.
Für all die verschiedenen Möglichkeiten, wie Ihre Fallklasse verwendet werden kann (wohlwollend und / oder böswillig), müssen Sie folgende Maßnahmen ergreifen:
- Für Ihr explizites Begleitobjekt:
- Erstellen Sie es mit genau demselben Namen wie Ihre Fallklasse
- Dies hat Zugriff auf die privaten Teile der Fallklasse
- Erstellen Sie eine
apply
Methode mit genau derselben Signatur wie der primäre Konstruktor für Ihre Fallklasse
- Dies wird erfolgreich kompiliert, sobald Schritt 2.1 abgeschlossen ist
- Stellen Sie eine Implementierung bereit, die mithilfe des
new
Operators eine Instanz der Fallklasse abruft und eine leere Implementierung bereitstellt{}
- Dadurch wird die Fallklasse nun streng zu Ihren Bedingungen instanziiert
- Die leere Implementierung
{}
muss bereitgestellt werden, da die Fallklasse deklariert ist abstract
(siehe Schritt 2.1).
- Für Ihre Fallklasse:
- Erkläre es
abstract
- Verhindert, dass der Scala-Compiler eine
apply
Methode im Begleitobjekt generiert, die den Kompilierungsfehler "Methode wird zweimal definiert ..." verursacht hat (Schritt 1.2 oben).
- Markieren Sie den primären Konstruktor als
private[A]
- Der primäre Konstruktor ist jetzt nur für die Fallklasse selbst und für ihr Begleitobjekt verfügbar (das oben in Schritt 1.1 definierte).
- Erstellen Sie eine
readResolve
Methode
- Stellen Sie eine Implementierung mit der Methode apply bereit (Schritt 1.2 oben).
- Erstellen Sie eine
copy
Methode
- Definieren Sie es so, dass es genau dieselbe Signatur wie der primäre Konstruktor der Fallklasse hat
- Für jeden Parameter, fügen Sie einen Standardwert unter Verwendung der gleichen Parameternamen (zB:
s: String = s
)
- Stellen Sie eine Implementierung mit der Apply-Methode bereit (Schritt 1.2 unten).
Hier ist Ihr Code, der mit den oben genannten Aktionen geändert wurde:
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)
}
Und hier ist Ihr Code, nachdem Sie die Anforderung implementiert haben (vorgeschlagen in der @ kollekullberg-Antwort) und den idealen Ort für jede Art von Caching identifiziert haben:
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)
}
Und diese Version ist sicherer / robuster, wenn dieser Code über Java Interop verwendet wird (versteckt die case-Klasse als Implementierung und erstellt eine endgültige Klasse, die Ableitungen verhindert):
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)
}
Während dies Ihre Frage direkt beantwortet, gibt es noch mehr Möglichkeiten, diesen Pfad um Fallklassen über das Instanz-Caching hinaus zu erweitern. Für meine eigenen Projektanforderungen habe ich eine noch umfassendere Lösung erstellt, die ich auf CodeReview (einer StackOverflow-Schwestersite) dokumentiert habe . Wenn Sie am Ende darüber nachdenken, meine Lösung nutzen oder nutzen, sollten Sie mir Feedback, Vorschläge oder Fragen hinterlassen. Ich werde mein Bestes tun, um innerhalb eines Tages zu antworten.