Alors voici la situation. Je veux définir une classe de cas comme suit:
case class A(val s: String)
et je veux définir un objet pour m'assurer que lorsque je crée des instances de la classe, la valeur de 's' est toujours en majuscule, comme suit:
object A {
def apply(s: String) = new A(s.toUpperCase)
}
Cependant, cela ne fonctionne pas car Scala se plaint que la méthode apply (s: String) est définie deux fois. Je comprends que la syntaxe de la classe de cas le définira automatiquement pour moi, mais n’y at-il pas un autre moyen de réaliser cela? J'aimerais rester avec la classe de cas puisque je veux l'utiliser pour la correspondance de motifs.
La raison du conflit est que la classe de cas fournit exactement la même méthode apply () (même signature).
Tout d’abord, je voudrais vous suggérer d’utiliser:
case class A(s: String) {
require(! s.toCharArray.exists( _.isLower ), "Bad string: "+ s)
}
Cela déclenchera une exception si l'utilisateur essaie de créer une instance dans laquelle s inclut des caractères minuscules. C'est une bonne utilisation des classes de cas, car ce que vous mettez dans le constructeur correspond également à ce que vous obtenez lorsque vous utilisez la correspondance de modèle (match
).
Si ce n'est pas ce que vous voulez, je ferais alors au constructeur private
et forcerais les utilisateurs à seulement utiliser la méthode apply:
class A private (val s: String) {
}
object A {
def apply(s: String): A = new A(s.toUpperCase)
}
Comme vous le voyez, A n'est plus un case class
. Je ne sais pas si les classes de cas avec des champs immuables sont destinées à la modification des valeurs entrantes, car le nom "classe de cas" implique qu'il devrait être possible d'extraire les arguments du constructeur (non modifiés) à l'aide de match
.
UPDATE 2016/02/25:
Bien que la réponse que j'ai donnée ci-dessous reste suffisante, il vaut également la peine de faire référence à une autre réponse connexe concernant l'objet compagnon de la classe de cas. À savoir, comment reproduire exactement l’objet compagnon implicite généré par le compilateur qui se produit lorsque l’on ne définit que la classe de cas elle-même. Pour moi, cela s'est avéré contre-intuitif.
Résumé:
Vous pouvez modifier la valeur d'un paramètre de classe de cas avant qu'il ne soit stocké dans la classe de cas assez simplement tout en restant un type de données abstrait (ADT) valide. La solution était relativement simple, mais la découverte des détails était un peu plus complexe.
Détails:
Si vous voulez vous assurer que seules des instances valides de votre classe de cas peuvent être instanciées, ce qui est une hypothèse essentielle derrière un ADT (type de données abstrait), vous devez effectuer un certain nombre de choses.
Par exemple, une méthode copy
générée par le compilateur est fournie par défaut sur une classe de cas. Ainsi, même si vous aviez soin de vous assurer que seules des instances ont été créées via la méthode apply
de l'objet compagnon explicite garantissant qu'elles ne peuvent contenir que des valeurs en majuscule, le code suivant générera une instance de classe avec une valeur en minuscule:
val a1 = A("Hi There") //contains "HI THERE"
val a2 = a1.copy(s = "gotcha") //contains "gotcha"
De plus, les classes de cas implémentent Java.io.Serializable
. Cela signifie que votre stratégie judicieuse consistant à n’avoir que des instances en majuscule peut être subvertie avec un simple éditeur de texte et une désérialisation.
Ainsi, quelle que soit la manière dont votre classe de cas peut être utilisée (avec bienveillance et/ou malveillance), voici les actions que vous devez entreprendre:
apply
avec exactement la même signature que le constructeur principal pour votre classe de cas new
et fournissant une implémentation vide {}
{}
doit être fournie car la classe de cas est déclarée abstract
(voir l'étape 2.1). abstract
apply
dans l'objet compagnon qui était à l'origine de la "la méthode est définie deux fois ..." erreur de compilation (étape 1.2 ci-dessus) private[A]
readResolve
copy
s: String = s
) Voici votre code modifié avec les actions ci-dessus:
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)
}
Et voici votre code après avoir implémenté le require (suggéré dans la réponse @ollekullberg) et identifié le lieu idéal pour mettre en cache toute sorte de cache:
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)
}
Et cette version est plus sécurisée/robuste si ce code sera utilisé via Java interop (cache la classe de cas en tant qu’implémentation et crée une classe finale qui empêche les dérivations):
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)
}
Bien que cela réponde directement à votre question, il existe encore plus de moyens d’élargir ce chemin autour des classes de cas au-delà de la mise en cache d’instances. Pour les besoins de mon propre projet, j'ai créé une solution encore plus vaste que j'ai documentée sur CodeReview (un site frère de StackOverflow). Si vous finissez par examiner, utiliser ou utiliser ma solution, envisagez de me laisser des commentaires, des suggestions ou des questions et, dans les limites du raisonnable, je ferai de mon mieux pour vous répondre dans les 24 heures.
Je ne sais pas comment redéfinir la méthode apply
dans l'objet compagnon (si cela est même possible), mais vous pouvez également utiliser un type spécial pour les chaînes en majuscule:
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"))
Le code ci-dessus renvoie:
A(HELLO)
Vous devriez également jeter un œil à cette question et à ses réponses: Scala: est-il possible de remplacer le constructeur de la classe de cas par défaut?
Pour ceux qui liront ceci après avril 2017: À partir de Scala 2.12.2+, Scala permet de passer outre à appliquer et de ne pas appliquer par défaut . Vous pouvez obtenir ce comportement en donnant l'option -Xsource:2.12
au compilateur sur Scala 2.11.11+.
Une autre idée, tout en conservant la classe de cas et n’ayant pas de définition implicite ni autre constructeur, est de rendre la signature de apply
légèrement différente, mais du même point de vue de l’utilisateur. Quelque part, j’ai vu le tour implicite, mais je ne peux pas me souvenir de l’argument implicite, j’ai donc choisi Boolean
ici. Si quelqu'un peut m'aider et finir le tour ...
object A {
def apply(s: String)(implicit ev: Boolean) = new A(s.toLowerCase)
}
case class A(s: String)
Cela fonctionne avec des variables var:
case class A(var s: String) {
// Conversion
s = s.toUpperCase
}
Cette pratique est apparemment encouragée dans les classes de cas au lieu de définir un autre constructeur. Vois ici. . Lors de la copie d'un objet, vous conservez également les mêmes modifications.
J'ai rencontré le même problème et cette solution me convient:
sealed trait A {
def s:String
}
object A {
private case class AImpl(s:String)
def apply(s:String):A = AImpl(s.toUpperCase)
}
Et, si une méthode est nécessaire, définissez-la dans le trait et remplacez-la dans la classe de cas.
Si vous êtes coincé avec une scala plus ancienne pour laquelle vous ne pouvez pas remplacer par défaut ou si vous ne souhaitez pas ajouter l'indicateur de compilation comme indiqué par @ mehmet-emre et si vous avez besoin d'une classe de cas, vous pouvez procéder comme suit:
case class A(private val _s: String) {
val s = _s.toUpperCase
}