86 votes

Comment remplacer une application dans un compagnon de classe de cas

Donc voici la situation. Je veux définir une classe de cas comme ceci:

case class A(val s: String)

et je veux définir un objet pour s'assurer que, lorsque je créer des instances de la classe, la valeur de 's' est toujours en majuscules, comme suit:

object A {
  def apply(s: String) = new A(s.toUpperCase)
}

Toutefois, cela ne fonctionne pas depuis la Scala est à se plaindre de l'appliquer(s: String) la méthode est définie deux fois. Je comprends que le cas de la classe de la syntaxe automatiquement le définir pour moi, mais n'est-il pas un autre moyen que je peux accomplir cela? Je voudrais coller avec la classe de cas, car je veux l'utiliser pour le filtrage.

90voto

olle kullberg Points 4916

La raison pour laquelle le conflit est-ce le cas de la classe fournit exactement la même appliquer (), méthode (même signature).

Tout d'abord, je tiens à vous suggérons d'utiliser exiger:

case class A(s: String) {
  require(! s.toCharArray.exists( _.isLower ), "Bad string: "+ s)
}

Cela permettra de lever une Exception si l'utilisateur tente de créer une instance où s intègre en minuscules caractères. C'est une bonne utilisation de classes de cas, depuis que vous avez mis dans le constructeur est aussi ce que vous obtenez lorsque vous utilisez le pattern matching (match).

Si ce n'est pas ce que vous voulez, puis je ferais le constructeur private et de forcer 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 suis pas sûr si des classes de cas avec immuables champs sont destinées à la modification de l'entrée de valeurs, depuis le nom de "classe de cas" implique qu'il devrait être possible d'extraire (non modifié) arguments du constructeur à l'aide de match.

28voto

Résumé:
Vous pouvez modifier la valeur d'une classe de cas de paramètre avant d'être stocké dans le cas de la classe assez simplement tout en restant valide(auf s i indiqué) ADT (Type Abstrait de Données). Alors que la solution est relativement simple, en découvrant les détails a été un peu plus difficile.

Détails:
Si vous voulez vous assurer valable uniquement les instances de votre classe de cas peut jamais être instancié qui est un postulat essentiel derrière un ADT (Type Abstrait de Données), il existe un certain nombre de choses que vous devez faire.

Par exemple, un générés par le compilateur copy méthode est fournie par défaut sur une classe de cas. Donc, même si vous êtes très prudent afin de s'assurer que seules les instances ont été créées par le biais de l'explicite compagnon de l'objet apply méthode qui garantit qu'ils ne pourrait jamais contenir supérieure aux valeurs, le code suivant devrait produire un cas instance de classe avec un bas de cas de la valeur:

val a1 = A("Hi There") //contains "HI THERE"
val a2 = a1.copy(s = "gotcha") //contains "gotcha"

En outre, des classes de cas de mettre en oeuvre java.io.Serializable. Cela signifie que votre œuvre d'une stratégie prudente d'avoir seulement majuscules instances peuvent être subverti avec un simple éditeur de texte et de désérialisation.

Ainsi, par tous les moyens divers de votre classe de cas peut être utilisé (avec bienveillance et/ou malveillante), voici les actions que vous devez prendre:

  1. Pour votre explicite compagnon de l'objet:
    1. Créer en utilisant exactement le même nom que la classe de cas de
      • Cela a accès à la classe de cas de ses parties intimes
    2. Créer un apply méthode avec exactement la même signature que le principal constructeur de votre classe de cas de
      • Cela permettra de compiler avec succès une fois l'étape 2.1 est terminé
    3. Fournir une implémentation de l'obtention d'une instance de la classe de cas à l'aide de l' new de l'opérateur et de fournir un vide de la mise en œuvre {}
      • Ce sera désormais instancier la classe de cas strictement à vos conditions
      • Le vide de la mise en œuvre {} doivent être fournis en raison de l'affaire de la classe est déclarée abstract (voir l'étape 2.1)
  2. Pour votre cas de la classe:
    1. Déclarer abstract
      • Empêche la Scala compilateur de générer un apply méthode dans le compagnon de l'objet qui est ce qui a provoqué le "la méthode est définie deux fois..." erreur de compilation (étape 1.2 ci-dessus)
    2. Marquer le principal constructeur comme private[A]
      • Le principal constructeur est maintenant disponible uniquement pour le cas de la classe elle-même et à son compagnon, l'objet (celui que nous avons défini ci-dessus dans l'étape 1.1)
    3. Créer un readResolveméthode
      1. Fournir une implémentation à l'aide de la méthode apply (étape 1.2 ci-dessus)
    4. Créer un copy méthode
      1. Définir exactement la même signature que le cas de la classe primaire constructeur
      2. Pour chaque paramètre, ajouter une valeur par défaut en utilisant le même nom de paramètre (ex: s: String = s)
      3. Fournir une implémentation à l'aide de la méthode apply (étape 1.2 ci-dessous)

Voici ton 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 la mise en œuvre de l'exiger (suggéré dans le @ollekullberg réponse) et aussi d'identifier l'endroit idéal pour mettre toute sorte de mise en 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é et robuste si ce code sera utilisé par Java interop (cache le cas de la classe comme une mise en place et crée une classe finale qui l'empêche de 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épond directement à votre question, il y a encore plus de moyens pour développer cette voie autour des classes de cas au-delà de l'instance de mise en cache. Pour mes propres besoins du projet, j'ai créé un système solution que j'ai documenté sur CodeReview (un StackOverflow site de soeur). Si vous êtes à la recherche là-dessus, à l'aide de ou de mettre à profit ma solution, merci d'envisager de me laisser des commentaires, des suggestions ou des questions et dans la raison, je vais faire de mon mieux pour répondre dans un délai de un jour.

12voto

Frank S. Thomas Points 2071

Je ne sais pas comment remplacer 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 consulter cette question et ses réponses: Scala: est-il possible de remplacer le constructeur de classe de cas par défaut?

5voto

Mikaël Mayer Points 2408

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.

4voto

Peter Schmitz Points 3221

Une autre idée, tout en conservant la classe de cas et en 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 quel argument implicite, c’est pourquoi j’ai 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)
 

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X