Quelle est la différence entre la classe de cas de Scala et la classe?


440

J'ai cherché dans Google pour trouver les différences entre a case classet a class. Tout le monde mentionne que lorsque vous souhaitez effectuer une correspondance de modèle sur la classe, utilisez la classe de cas. Sinon, utilisez des classes et mentionnez également des avantages supplémentaires tels que l'égalité et le remplacement du code de hachage. Mais sont-ce les seules raisons pour lesquelles on devrait utiliser une classe case au lieu d'une classe?

Je suppose qu'il devrait y avoir une raison très importante pour cette fonctionnalité dans Scala. Quelle est l'explication ou existe-t-il une ressource pour en savoir plus sur les classes de cas Scala?

Réponses:


394

Les classes de cas peuvent être considérées comme des objets de stockage de données simples et immuables qui devraient dépendre exclusivement de leurs arguments de constructeur .

Ce concept fonctionnel nous permet de

  • utiliser une syntaxe d'initialisation compacte ( Node(1, Leaf(2), None)))
  • les décomposer en utilisant la correspondance de motifs
  • avoir des comparaisons d'égalité définies implicitement

En combinaison avec l'héritage, les classes de cas sont utilisées pour imiter les types de données algébriques .

Si un objet effectue des calculs avec état à l'intérieur ou présente d'autres types de comportements complexes, il doit s'agir d'une classe ordinaire.


11
@Teja: D'une certaine manière. Les ADT sont des énumérations un peu paramétrées , extrêmement puissantes et sécuritaires.
Dario

8
Les classes de cas scellés sont utilisées pour imiter les types de données algébriques. Sinon, le nombre de sous-classes n'est pas limité.
Thomas Jung

6
@ Thomas: Correctement parlées, les classes de cas dérivées de classes abstraites scellées imitent les types de données algébriques fermés alors que l'ADT est autrement ouvert .
Dario

2
@Dario ... et le type est autrement ouvert et non et un ADT. :-)
Thomas Jung

1
@Thomas: Oui, c'est simplement un existentiel;)
Dario

165

Techniquement, il n'y a pas de différence entre une classe et une classe de cas - même si le compilateur optimise certaines choses lors de l'utilisation des classes de cas. Cependant, une classe de cas est utilisée pour supprimer la plaque de chaudière pour un modèle spécifique, qui implémente des types de données algébriques .

Les arbres sont un exemple très simple de ces types. Un arbre binaire, par exemple, peut être implémenté comme ceci:

sealed abstract class Tree
case class Node(left: Tree, right: Tree) extends Tree
case class Leaf[A](value: A) extends Tree
case object EmptyLeaf extends Tree

Cela nous permet de faire ce qui suit:

// DSL-like assignment:
val treeA = Node(EmptyLeaf, Leaf(5))
val treeB = Node(Node(Leaf(2), Leaf(3)), Leaf(5))

// On Scala 2.8, modification through cloning:
val treeC = treeA.copy(left = treeB.left)

// Pretty printing:
println("Tree A: "+treeA)
println("Tree B: "+treeB)
println("Tree C: "+treeC)

// Comparison:
println("Tree A == Tree B: %s" format (treeA == treeB).toString)
println("Tree B == Tree C: %s" format (treeB == treeC).toString)

// Pattern matching:
treeA match {
  case Node(EmptyLeaf, right) => println("Can be reduced to "+right)
  case Node(left, EmptyLeaf) => println("Can be reduced to "+left)
  case _ => println(treeA+" cannot be reduced")
}

// Pattern matches can be safely done, because the compiler warns about
// non-exaustive matches:
def checkTree(t: Tree) = t match {
  case Node(EmptyLeaf, Node(left, right)) =>
  // case Node(EmptyLeaf, Leaf(el)) =>
  case Node(Node(left, right), EmptyLeaf) =>
  case Node(Leaf(el), EmptyLeaf) =>
  case Node(Node(l1, r1), Node(l2, r2)) =>
  case Node(Leaf(e1), Leaf(e2)) =>
  case Node(Node(left, right), Leaf(el)) =>
  case Node(Leaf(el), Node(left, right)) =>
  // case Node(EmptyLeaf, EmptyLeaf) =>
  case Leaf(el) =>
  case EmptyLeaf =>
}

Notez que les arbres construisent et déconstruisent (par correspondance de motifs) avec la même syntaxe, qui est également exactement la façon dont ils sont imprimés (espaces moins).

Et ils peuvent également être utilisés avec des cartes ou des ensembles de hachage, car ils ont un hashCode valide et stable.


71
  • Les classes de cas peuvent être appariées par modèle
  • Les classes de cas définissent automatiquement le hashcode et égal
  • Les classes de cas définissent automatiquement des méthodes getter pour les arguments du constructeur.

(Vous avez déjà mentionné tout sauf le dernier).

Ce sont les seules différences par rapport aux classes ordinaires.


13
Les setters ne sont pas générés pour les classes de cas sauf si "var" est spécifié dans l'argument constructeur, auquel cas vous obtenez la même génération getter / setter que les classes régulières.
Mitch Blevins

1
@Mitch: C'est vrai, mon mauvais. Fixé maintenant.
sepp2k

Vous avez omis 2 différences, voir ma réponse.
Shelby Moore III

@MitchBlevins, les classes régulières n'ont pas toujours de génération getter / setter.
Shelby Moore III,

Les classes de cas définissent une méthode inappliquée, c'est pourquoi elles peuvent être mises en correspondance de motifs.
Happy Torturer

30

Personne n'a mentionné que les classes de cas sont également des instances Productet héritent donc de ces méthodes:

def productElement(n: Int): Any
def productArity: Int
def productIterator: Iterator[Any]

productArityretourne le nombre de paramètres de classe, productElement(i)retourne le i ème paramètre et productIteratorpermet de les parcourir.


2
Cependant, ce ne sont pas des instances de Product1, Product2, etc.
Jean-Philippe Pellet

27

Personne n'a mentionné que les classes de cas ont valdes paramètres de constructeur, mais c'est également la valeur par défaut pour les classes régulières (ce qui, je pense, est une incohérence dans la conception de Scala). Dario a laissé entendre qu'il en était ainsi " immuable ".

Notez que vous pouvez remplacer la valeur par défaut en ajoutant à chaque argument constructeur les varclasses de cas. Cependant, la modification des classes de cas entraîne une variation temporelle de leurs méthodes equalset hashCode. [1]

sepp2k a déjà mentionné que les classes de cas génèrent automatiquement des méthodes equalset hashCode.

Pas aussi un indiqué que les classes de cas créent automatiquement un compagnon objectavec le même nom que la classe, qui contient applyet unapplyméthodes. La applyméthode permet de construire des instances sans ajouter de préfixe new. La unapplyméthode d'extraction permet la correspondance de motifs que d'autres ont mentionnée.

De plus , le compilateur optimise la vitesse de match- caseappariement de formes pour les classes de cas [2].

[1] Les classes de cas sont cool

[2] Classes de cas et extracteurs, p . 15 .


12

La construction de classe de cas dans Scala peut également être considérée comme une commodité pour supprimer un passe-partout.

Lors de la construction d'une classe de cas, Scala vous donne ce qui suit.

  • Il crée une classe ainsi que son objet compagnon
  • Son objet compagnon implémente la applyméthode que vous pouvez utiliser comme méthode d'usine. Vous obtenez l'avantage syntaxique du sucre de ne pas avoir à utiliser le nouveau mot clé.

Parce que la classe est immuable, vous obtenez des accesseurs, qui ne sont que les variables (ou propriétés) de la classe mais pas de mutateurs (donc pas de possibilité de changer les variables). Les paramètres du constructeur sont automatiquement disponibles en tant que champs publics en lecture seule. Beaucoup plus agréable à utiliser que la construction de bean Java.

  • Vous pouvez également obtenir hashCode, equalset les toStringméthodes par défaut et la equalsméthode compare un objet structurellement. Une copyméthode est générée pour pouvoir cloner un objet (certains champs ayant de nouvelles valeurs fournies à la méthode).

Le plus grand avantage, comme cela a été mentionné précédemment, est le fait que vous pouvez faire correspondre les modèles sur les classes de cas. La raison en est que vous obtenez la unapplyméthode qui vous permet de déconstruire une classe de cas pour extraire ses champs.


En substance, ce que vous obtenez de Scala lors de la création d'une classe de cas (ou d'un objet cas si votre classe ne prend aucun argument) est un objet singleton qui sert à la fois de fabrique et d' extracteur .


Pourquoi auriez-vous besoin d'une copie d'un objet immuable?
Paŭlo Ebermann

@ PaŭloEbermann Parce que la copyméthode peut modifier les champs:val x = y.copy(foo="newValue")
Thilo

8

Outre ce que les gens ont déjà dit, il existe des différences plus fondamentales entre classetcase class

1. Case Classn'a pas besoin d'être explicite new, alors que la classe doit être appelée avecnew

val classInst = new MyClass(...)  // For classes
val classInst = MyClass(..)       // For case class

2.Par défaut, les paramètres des constructeurs sont privés dans class, tandis que son public danscase class

// For class
class MyClass(x:Int) { }
val classInst = new MyClass(10)

classInst.x   // FAILURE : can't access

// For caseClass
case class MyClass(x:Int) { }
val classInst = MyClass(10)

classInst.x   // SUCCESS

3. se case classcomparer par valeur

// case Class
class MyClass(x:Int) { }

val classInst = new MyClass(10)
val classInst2 = new MyClass(10)

classInst == classInst2 // FALSE

// For Case Class
case class MyClass(x:Int) { }

val classInst = MyClass(10)
val classInst2 = MyClass(10)

classInst == classInst2 // TRUE

6

Selon la documentation de Scala :

Les classes de cas ne sont que des classes régulières qui sont:

  • Immuable par défaut
  • Décomposable par correspondance de motifs
  • Comparé à l'égalité structurelle plutôt qu'à la référence
  • Succinct pour instancier et opérer

Une autre caractéristique du mot clé case est que le compilateur génère automatiquement plusieurs méthodes pour nous, y compris les méthodes familières toString, equals et hashCode en Java.


5

Classe:

scala> class Animal(name:String)
defined class Animal

scala> val an1 = new Animal("Padddington")
an1: Animal = Animal@748860cc

scala> an1.name
<console>:14: error: value name is not a member of Animal
       an1.name
           ^

Mais si nous utilisons le même code mais que nous utilisons la classe de cas:

scala> case class Animal(name:String)
defined class Animal

scala> val an2 = new Animal("Paddington")
an2: Animal = Animal(Paddington)

scala> an2.name
res12: String = Paddington


scala> an2 == Animal("fred")
res14: Boolean = false

scala> an2 == Animal("Paddington")
res15: Boolean = true

Classe de personne:

scala> case class Person(first:String,last:String,age:Int)
defined class Person

scala> val harry = new Person("Harry","Potter",30)
harry: Person = Person(Harry,Potter,30)

scala> harry
res16: Person = Person(Harry,Potter,30)
scala> harry.first = "Saily"
<console>:14: error: reassignment to val
       harry.first = "Saily"
                   ^
scala>val saily =  harry.copy(first="Saily")
res17: Person = Person(Saily,Potter,30)

scala> harry.copy(age = harry.age+1)
res18: Person = Person(Harry,Potter,31)

Correspondance de motif:

scala> harry match {
     | case Person("Harry",_,age) => println(age)
     | case _ => println("no match")
     | }
30

scala> res17 match {
     | case Person("Harry",_,age) => println(age)
     | case _ => println("no match")
     | }
no match

objet: singleton:

scala> case class Person(first :String,last:String,age:Int)
defined class Person

scala> object Fred extends Person("Fred","Jones",22)
defined object Fred

5

Pour avoir la compréhension ultime de ce qu'est une classe de cas:

supposons la définition de classe de cas suivante:

case class Foo(foo:String, bar: Int)

puis procédez comme suit dans le terminal:

$ scalac -print src/main/scala/Foo.scala

Scala 2.12.8 affichera:

...
case class Foo extends Object with Product with Serializable {

  <caseaccessor> <paramaccessor> private[this] val foo: String = _;

  <stable> <caseaccessor> <accessor> <paramaccessor> def foo(): String = Foo.this.foo;

  <caseaccessor> <paramaccessor> private[this] val bar: Int = _;

  <stable> <caseaccessor> <accessor> <paramaccessor> def bar(): Int = Foo.this.bar;

  <synthetic> def copy(foo: String, bar: Int): Foo = new Foo(foo, bar);

  <synthetic> def copy$default$1(): String = Foo.this.foo();

  <synthetic> def copy$default$2(): Int = Foo.this.bar();

  override <synthetic> def productPrefix(): String = "Foo";

  <synthetic> def productArity(): Int = 2;

  <synthetic> def productElement(x$1: Int): Object = {
    case <synthetic> val x1: Int = x$1;
        (x1: Int) match {
            case 0 => Foo.this.foo()
            case 1 => scala.Int.box(Foo.this.bar())
            case _ => throw new IndexOutOfBoundsException(scala.Int.box(x$1).toString())
        }
  };

  override <synthetic> def productIterator(): Iterator = scala.runtime.ScalaRunTime.typedProductIterator(Foo.this);

  <synthetic> def canEqual(x$1: Object): Boolean = x$1.$isInstanceOf[Foo]();

  override <synthetic> def hashCode(): Int = {
     <synthetic> var acc: Int = -889275714;
     acc = scala.runtime.Statics.mix(acc, scala.runtime.Statics.anyHash(Foo.this.foo()));
     acc = scala.runtime.Statics.mix(acc, Foo.this.bar());
     scala.runtime.Statics.finalizeHash(acc, 2)
  };

  override <synthetic> def toString(): String = scala.runtime.ScalaRunTime._toString(Foo.this);

  override <synthetic> def equals(x$1: Object): Boolean = Foo.this.eq(x$1).||({
      case <synthetic> val x1: Object = x$1;
        case5(){
          if (x1.$isInstanceOf[Foo]())
            matchEnd4(true)
          else
            case6()
        };
        case6(){
          matchEnd4(false)
        };
        matchEnd4(x: Boolean){
          x
        }
    }.&&({
      <synthetic> val Foo$1: Foo = x$1.$asInstanceOf[Foo]();
      Foo.this.foo().==(Foo$1.foo()).&&(Foo.this.bar().==(Foo$1.bar())).&&(Foo$1.canEqual(Foo.this))
  }));

  def <init>(foo: String, bar: Int): Foo = {
    Foo.this.foo = foo;
    Foo.this.bar = bar;
    Foo.super.<init>();
    Foo.super./*Product*/$init$();
    ()
  }
};

<synthetic> object Foo extends scala.runtime.AbstractFunction2 with Serializable {

  final override <synthetic> def toString(): String = "Foo";

  case <synthetic> def apply(foo: String, bar: Int): Foo = new Foo(foo, bar);

  case <synthetic> def unapply(x$0: Foo): Option =
     if (x$0.==(null))
        scala.None
     else
        new Some(new Tuple2(x$0.foo(), scala.Int.box(x$0.bar())));

  <synthetic> private def readResolve(): Object = Foo;

  case <synthetic> <bridge> <artifact> def apply(v1: Object, v2: Object): Object = Foo.this.apply(v1.$asInstanceOf[String](), scala.Int.unbox(v2));

  def <init>(): Foo.type = {
    Foo.super.<init>();
    ()
  }
}
...

Comme nous pouvons le voir, le compilateur Scala produit une classe régulière Fooet un objet compagnon Foo.

Passons en revue la classe compilée et commentons ce que nous avons:

  • l'état interne de la Fooclasse, immuable:
val foo: String
val bar: Int
  • getters:
def foo(): String
def bar(): Int
  • méthodes de copie:
def copy(foo: String, bar: Int): Foo
def copy$default$1(): String
def copy$default$2(): Int
  • scala.Producttrait d' implémentation :
override def productPrefix(): String
def productArity(): Int
def productElement(x$1: Int): Object
override def productIterator(): Iterator
  • implémentation de scala.Equalstrait pour rendre les instances de classe de cas comparables pour l'égalité par ==:
def canEqual(x$1: Object): Boolean
override def equals(x$1: Object): Boolean
  • remplacement java.lang.Object.hashCodepour obéir au contrat equals-hashcode:
override <synthetic> def hashCode(): Int
  • prioritaire java.lang.Object.toString:
override def toString(): String
  • constructeur pour l'instanciation par newmot-clé:
def <init>(foo: String, bar: Int): Foo 

Object Foo: - méthode applyd'instanciation sans newmot-clé:

case <synthetic> def apply(foo: String, bar: Int): Foo = new Foo(foo, bar);
  • méthode d'extraction unupplypour utiliser la classe de cas Foo dans la correspondance de motifs:
case <synthetic> def unapply(x$0: Foo): Option
  • méthode pour protéger l'objet en tant que singleton de la désérialisation pour ne pas laisser produire une instance de plus:
<synthetic> private def readResolve(): Object = Foo;
  • objet Foo étend scala.runtime.AbstractFunction2pour faire une telle astuce:
scala> case class Foo(foo:String, bar: Int)
defined class Foo

scala> Foo.tupled
res1: ((String, Int)) => Foo = scala.Function2$$Lambda$224/1935637221@9ab310b

tupled from object retourne une fonction pour créer un nouveau Foo en appliquant un tuple de 2 éléments.

La classe de cas n'est donc que du sucre syntaxique.


4

Contrairement aux classes, les classes de cas ne sont utilisées que pour conserver des données.

Les classes de cas sont flexibles pour les applications centrées sur les données, ce qui signifie que vous pouvez définir des champs de données dans la classe de cas et définir la logique métier dans un objet compagnon. De cette façon, vous séparez les données de la logique métier.

Avec la méthode de copie, vous pouvez hériter une ou toutes les propriétés requises de la source et les modifier à votre guise.


3

Personne n'a mentionné que l'objet compagnon de classe de cas a une tupleddéfense, qui a un type:

case class Person(name: String, age: Int)
//Person.tupled is def tupled: ((String, Int)) => Person

Le seul cas d'utilisation que je peux trouver est lorsque vous devez construire une classe de cas à partir de tuple, par exemple:

val bobAsTuple = ("bob", 14)
val bob = (Person.apply _).tupled(bobAsTuple) //bob: Person = Person(bob,14)

Vous pouvez faire de même, sans tupled, en créant directement un objet, mais si vos jeux de données exprimés sous forme de liste de tuple avec arity 20 (tuple avec 20 éléments), vous pouvez utiliser tupled est votre choix.


3

Une classe de cas est une classe qui peut être utilisée avec l' match/caseinstruction.

def isIdentityFun(term: Term): Boolean = term match {
  case Fun(x, Var(y)) if x == y => true
  case _ => false
}

Tu vois ça case est suivi d'une instance de classe Fun dont le 2e paramètre est un Var. Il s'agit d'une syntaxe très agréable et puissante, mais elle ne peut pas fonctionner avec les instances de n'importe quelle classe, il existe donc des restrictions pour les classes de cas. Et si ces restrictions sont respectées, il est possible de définir automatiquement le hashcode et les égaux.

L'expression vague "un mécanisme de décomposition récursive via la correspondance de motifs" signifie simplement "qu'il fonctionne avec case". (En effet, l'instance suivie matchest comparée (comparée à) l'instance suivante case, Scala doit les décomposer tous les deux et décomposer récursivement ce dont ils sont faits.)

Quelles classes de cas sont utiles? L' article de Wikipedia sur les types de données algébriques donne deux bons exemples classiques, des listes et des arbres. La prise en charge des types de données algébriques (y compris savoir comment les comparer) est un must pour tout langage fonctionnel moderne.

Pour quelles classes de cas ne sont pas utiles? Certains objets ont un état, le code connection.setConnectTimeout(connectTimeout)n'est pas pour les classes de cas.

Et maintenant, vous pouvez lire A Tour of Scala: Case Classes


2

Je pense que dans l'ensemble toutes les réponses ont donné une explication sémantique sur les classes et les classes de cas. Cela pourrait être très pertinent, mais chaque débutant dans scala devrait savoir ce qui se passe lorsque vous créez une classe de cas. J'ai écrit ceci réponse, qui explique la classe de cas en un mot.

Chaque programmeur doit savoir que s'il utilise des fonctions prédéfinies, il écrit un code relativement moins, ce qui lui permet de donner le pouvoir d'écrire le code le plus optimisé, mais le pouvoir s'accompagne de grandes responsabilités. Donc, utilisez des fonctions prédéfinies avec beaucoup de précautions.

Certains développeurs évitent d'écrire des classes de casse en raison de 20 méthodes supplémentaires, que vous pouvez voir en désassemblant le fichier de classe.

Veuillez vous référer à ce lien si vous souhaitez vérifier toutes les méthodes à l'intérieur d'une classe de cas .


1
  • Les classes de cas définissent un objet compagnon avec les méthodes apply et unapply
  • Les classes de cas étendent Serializable
  • Les classes de cas définissent hashCode et copient les méthodes
  • Tous les attributs du constructeur sont val (sucre syntaxique)

1

Certaines des fonctionnalités clés de case classessont répertoriées ci-dessous

  1. les classes de cas sont immuables.
  2. Vous pouvez instancier des classes de cas sans newmot-clé.
  3. les classes de cas peuvent être comparées par valeur

Exemple de code scala sur scala fiddle, tiré des documents scala.

https://scalafiddle.io/sf/34XEQyE/0

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.