J'ai cherché dans Google pour trouver les différences entre un case class
et un class
. Tout le monde mentionne que lorsque vous souhaitez effectuer une correspondance de modèle dans la classe, utilisez la classe de cas. Sinon, utilisez les classes et mentionnez également des avantages supplémentaires, tels qu'égal et le code de hachage. Mais sont-ce les seules raisons pour lesquelles on devrait utiliser une classe de cas au lieu de 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?
Les classes de cas peuvent être vues comme des objets simples et immuables contenant des données, qui doivent exclusivement dépendre de leurs arguments de constructeur .
Ce concept fonctionnel nous permet de
Node(1, Leaf(2), None))
)En combinaison avec l'héritage, les classes de cas sont utilisées pour imiter types de données algébriques .
Si un objet effectue des calculs avec état à l'intérieur ou présente d'autres types de comportement complexe, il doit s'agir d'une classe ordinaire.
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 lorsqu'il utilise 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 types de données algébriques .
Un exemple très simple de tels types sont les arbres. 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 modèle) avec la même syntaxe, qui correspond également à la manière dont ils sont imprimés (espaces moins).
Et ils peuvent également être utilisés avec des cartes de hachage ou des ensembles, car ils ont un hashCode valide et stable.
(Vous avez déjà mentionné tout sauf le dernier).
Ce sont les seules différences aux classes ordinaires.
Personne n'a mentionné que les classes de cas sont également des instances de Product
et héritent donc de ces méthodes:
def productElement(n: Int): Any
def productArity: Int
def productIterator: Iterator[Any]
où productArity
renvoie le nombre de paramètres de classe, productElement(i)
renvoie le ith paramètre, et productIterator
permet de les parcourir.
Personne n'a mentionné que les classes de cas ont val
paramètres de constructeur, mais c'est aussi la valeur par défaut pour les classes régulières (qui je pense que c'est une incohérence dans la conception de Scala). Dario a laissé entendre qu'il était " immuable ".
Notez que vous pouvez remplacer la valeur par défaut en ajoutant le début de chaque argument de constructeur avec var
pour les classes de cas. Cependant, rendre les classes de cas mutables rend leurs méthodes equals
et hashCode
variant avec le temps. [1]
sepp2k déjà mentionné, les classes de cas génèrent automatiquement les méthodes equals
et hashCode
.
De plus, personne n'a mentionné que les classes de cas créent automatiquement un compagnon object
avec le même nom que la classe, qui contient les méthodes apply
et unapply
. La méthode apply
permet de construire des instances sans ajouter de préfixe avec new
. La méthode d'extraction unapply
active la correspondance de motif mentionnée par d'autres.
De plus, le compilateur optimise la vitesse de correspondance de modèle match
-case
pour les classes de cas [2].
La construction de classe de cas dans Scala peut également être considérée comme un moyen pratique de supprimer un certain standard.
Lors de la construction d'une classe de cas, Scala vous donne les informations suivantes.
apply
que vous pouvez utiliser comme méthode par défaut. Vous bénéficiez de l’avantage en sucre syntaxique 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 sous forme de champs en lecture seule. Bien plus agréable à utiliser que la construction de bean Java.
hashCode
, equals
et toString
par défaut et la méthode equals
compare structurellement un objet. Une méthode copy
est générée pour pouvoir cloner un objet (certains champs ayant de nouvelles valeurs fournies à la méthode).Le principal avantage, comme cela a été mentionné précédemment, est le fait que vous pouvez appliquer des motifs sur des classes de cas. La raison en est que vous obtenez la méthode unapply
qui vous permet de déconstruire une classe de cas pour extraire ses champs.
Essentiellement, ce que vous obtenez de Scala lors de la création d'une classe de cas (ou d'un objet de cas si votre classe ne prend pas d'argument) est un objet singleton qui remplit sa fonction en tant que fabrique et en tant que extracteur.
Outre ce que les gens ont déjà dit, il existe quelques différences plus fondamentales entre class
et case class
1 .Case Class
n'a pas besoin de new
explicite, alors que la classe doit être appelée avec new
val classInst = new MyClass(...) // For classes
val classInst = MyClass(..) // For case class
2.Les paramètres des constructeurs par défaut sont privés dans class
, alors que ses paramètres publics dans case 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 .case class
se comparent 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
Selon Scala's documentation :
Les classes de cas ne sont que des classes régulières qui sont:
- Immuable par défaut
- Décomposable à travers correspondance de motif
- Comparé par égalité structurelle plutôt que par 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, y compris les méthodes toString, equals et hashCode, bien connues en Java.
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 en utilisant 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 modèle:
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
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 produira:
...
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 constater, le compilateur Scala produit une classe régulière Foo
et un objet compagnon Foo
.
Passons en revue la classe compilée et commentons ce que nous avons:
Foo
, immuable:val foo: String
val bar: Int
def foo(): String
def bar(): Int
def copy(foo: String, bar: Int): Foo
def copy$default$1(): String
def copy$default$2(): Int
scala.Product
trait:override def productPrefix(): String
def productArity(): Int
def productElement(x$1: Int): Object
override def productIterator(): Iterator
scala.Equals
trait pour rendre les instances de classe de cas comparables pour égalité par ==
:def canEqual(x$1: Object): Boolean
override def equals(x$1: Object): Boolean
Java.lang.Object.hashCode
pour obéir au contrat de code de hachage:override <synthetic> def hashCode(): Int
Java.lang.Object.toString
:override def toString(): String
new
mot-clé:def <init>(foo: String, bar: Int): Foo
Objet Foo: - méthode apply
pour une instanciation sans new
mot-clé:
case <synthetic> def apply(foo: String, bar: Int): Foo = new Foo(foo, bar);
unupply
pour utiliser la classe de cas Foo dans la recherche de modèle:case <synthetic> def unapply(x$0: Foo): Option
<synthetic> private def readResolve(): Object = Foo;
scala.runtime.AbstractFunction2
pour avoir fait 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 renvoie une fonction permettant de créer un nouveau Foo en appliquant un tuple de 2 éléments.
Donc, la classe de cas est juste le sucre syntaxique.
Contrairement aux classes, les classes de cas ne sont utilisées que pour stocker 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 une classe de cas et définir une logique métier dans un objet compagnon. De cette manière, vous séparez les données de la logique applicative.
Avec la méthode copy, vous pouvez hériter de toutes les propriétés requises de la source et les modifier à votre guise.
Personne n'a mentionné que l'objet compagnon de classe de cas a tupled
defention, qui a le type:
case class Person(name: String, age: Int)
//Person.tupled is def tupled: ((String, Int)) => Person
Le seul cas d'utilisation que je puisse trouver est lorsqu'il est nécessaire de 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 la même chose, sans multiplier, en créant un objet directement, mais si vous préférez que vos ensembles de données soient exprimés sous forme de liste de Tuple avec arité 20 (Tuple avec 20 éléments), vous pouvez utiliser la syntaxe.
Une classe de cas est une classe pouvant être utilisée avec l'instruction match/case
.
def isIdentityFun(term: Term): Boolean = term match {
case Fun(x, Var(y)) if x == y => true
case _ => false
}
Vous voyez que case
est suivi d'une instance de la classe Fun dont le second paramètre est un Var. C'est une syntaxe très agréable et puissante, mais elle ne peut pas fonctionner avec les instances d'une classe, il y a donc des restrictions pour les classes de cas. Et si ces restrictions sont respectées, il est possible de définir automatiquement hashcode et equals.
L'expression vague "un mécanisme de décomposition récursif via un filtrage par motif" signifie simplement "cela fonctionne avec case
". (En effet, l'instance suivie par match
est comparée à (appariée à) l'instance qui suit case
, Scala doit les décomposer tous les deux et doit décomposer ce qu'ils sont récursivement. fait de.)
Quelles sont les classes de cas ? Le 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 les comparer) est indispensable pour tout langage fonctionnel moderne.
À quoi les classes de cas ne sont-elles pas utiles ? Certains objets ont un état, le code comme connection.setConnectTimeout(connectTimeout)
n'est pas destiné aux classes de cas.
Et maintenant, vous pouvez lire n tour de Scala: Classes d’affaires
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 this answer, ce qui explique la classe de cas en un mot.
Tous les programmeurs doivent savoir que s’ils utilisent des fonctions prédéfinies, ils écrivent un code comparativement moins important, ce qui leur permet de rédiger le code le plus optimisé, mais le pouvoir comporte 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 cas en raison de 20 méthodes supplémentaires, que vous pouvez voir en désassemblant un fichier de classe.
S'il vous plaît référez-vous à ce lien si vous souhaitez vérifier toutes les méthodes d'une classe de cas .
Certaines des principales caractéristiques de case classes
sont énumérées ci-dessous.
new
.Exemple de code scala sur scala fiddle, extrait du scala docs.