web-dev-qa-db-fra.com

Comment remplacer une application dans un compagnon de classe de cas

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.

75
John S

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

84
olle kullberg

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: 

  1. Pour votre objet compagnon explicite:
    1. Créez-le en utilisant exactement le même nom que votre classe de cas
      • Cela a accès aux parties privées de la classe de cas 
    2. Créez une méthode apply avec exactement la même signature que le constructeur principal pour votre classe de cas
      • Cela sera compilé avec succès une fois que l'étape 2.1 est terminée 
    3. Fournissez une implémentation obtenant une instance de la classe de cas à l'aide de l'opérateur new et fournissant une implémentation vide {}
      • Cela instanciera maintenant la classe de cas strictement selon vos conditions.
      • L'implémentation vide {} doit être fournie car la classe de cas est déclarée abstract (voir l'étape 2.1). 
  2. Pour votre classe de cas:
    1. Déclarez-le abstract
      • Empêche le compilateur Scala de générer une méthode 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) 
    2. Marquez le constructeur principal en tant que private[A]
      • Le constructeur principal n'est plus disponible que pour la classe de cas elle-même et pour son objet compagnon (celui défini ci-dessus à l'étape 1.1). 
    3. Créez une méthode readResolve
      1. Fournir une implémentation en utilisant la méthode apply (étape 1.2 ci-dessus)
    4. Créez une méthode copy
      1. Définissez-le pour qu'il ait exactement la même signature que le constructeur principal de la classe de cas 
      2. Pour chaque paramètre, ajoutez une valeur par défaut en utilisant le même nom de paramètre (ex: s: String = s
      3. Fournir une implémentation en utilisant la méthode apply (étape 1.2 ci-dessous) 

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.

25
chaotic3quilibrium

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?

11
Frank S. Thomas

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+.

6
Mehmet Emre

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)
4
Peter Schmitz

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.

4
Mikaël Mayer

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.

2
Pere Ramirez

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
}
0
critium