Scala Language
Classes de cas
Recherche…
Syntaxe
- case class Foo () // Les classes de cas sans paramètres doivent avoir une liste vide
- case class Foo (a1: A1, ..., aN: AN) // Crée une classe de cas avec les champs a1 ... aN
- objet case Bar // Créer une classe de cas singleton
Égalité de classe de cas
Une fonctionnalité fournie gratuitement par les classes de cas est une méthode equals
la valeur générée automatiquement qui vérifie l'égalité de valeur de tous les champs de membre individuels au lieu de vérifier uniquement l'égalité de référence des objets.
Avec des classes ordinaires:
class Foo(val i: Int)
val a = new Foo(3)
val b = new Foo(3)
println(a == b)// "false" because they are different objects
Avec des classes de cas:
case class Foo(i: Int)
val a = Foo(3)
val b = Foo(3)
println(a == b)// "true" because their members have the same value
Artefacts de code générés
Le modificateur de case
permet au compilateur Scala de générer automatiquement un code standard commun pour la classe. L'implémentation manuelle de ce code est fastidieuse et source d'erreurs. La définition de classe de cas suivante:
case class Person(name: String, age: Int)
... le code suivant sera automatiquement généré:
class Person(val name: String, val age: Int)
extends Product with Serializable
{
def copy(name: String = this.name, age: Int = this.age): Person =
new Person(name, age)
def productArity: Int = 2
def productElement(i: Int): Any = i match {
case 0 => name
case 1 => age
case _ => throw new IndexOutOfBoundsException(i.toString)
}
def productIterator: Iterator[Any] =
scala.runtime.ScalaRunTime.typedProductIterator(this)
def productPrefix: String = "Person"
def canEqual(obj: Any): Boolean = obj.isInstanceOf[Person]
override def hashCode(): Int = scala.runtime.ScalaRunTime._hashCode(this)
override def equals(obj: Any): Boolean = this.eq(obj) || obj match {
case that: Person => this.name == that.name && this.age == that.age
case _ => false
}
override def toString: String =
scala.runtime.ScalaRunTime._toString(this)
}
Le modificateur de case
génère également un objet compagnon:
object Person extends AbstractFunction2[String, Int, Person] with Serializable {
def apply(name: String, age: Int): Person = new Person(name, age)
def unapply(p: Person): Option[(String, Int)] =
if(p == null) None else Some((p.name, p.age))
}
Appliqué à un object
, le modificateur de case
a des effets similaires (quoique moins dramatiques). Ici, les gains principaux sont une implémentation de toString
et une valeur hashCode
cohérente entre les processus. Notez que les objets case (correctement) utilisent une égalité de référence:
object Foo extends Product with Serializable {
def productArity: Int = 0
def productIterator: Iterator[Any] =
scala.runtime.ScalaRunTime.typedProductIterator(this)
def productElement(i: Int): Any =
throw new IndexOutOfBoundsException(i.toString)
def productPrefix: String = "Foo"
def canEqual(obj: Any): Boolean = obj.isInstanceOf[this.type]
override def hashCode(): Int = 70822 // "Foo".hashCode()
override def toString: String = "Foo"
}
Il est toujours possible d'implémenter manuellement des méthodes qui seraient autrement fournies par le modificateur de case
dans la classe elle-même et dans son objet compagnon.
Les bases de la classe de cas
Par rapport aux classes ordinaires, la notation des classes de cas présente plusieurs avantages:
Tous les arguments de constructeur sont
public
et peuvent être accédés sur des objets initialisés (normalement ce n’est pas le cas, comme démontré ici):case class Dog1(age: Int) val x = Dog1(18) println(x.age) // 18 (success!) class Dog2(age: Int) val x = new Dog2(18) println(x.age) // Error: "value age is not a member of Dog2"
Il fournit une implémentation pour les méthodes suivantes:
toString
,equals
,hashCode
(basé sur les propriétés),copy
,apply
et ne pasunapply
:case class Dog(age: Int) val d1 = Dog(10) val d2 = d1.copy(age = 15)
Il fournit un mécanisme pratique pour la correspondance de modèle:
sealed trait Animal // `sealed` modifier allows inheritance within current build-unit only case class Dog(age: Int) extends Animal case class Cat(owner: String) extends Animal val x: Animal = Dog(18) x match { case Dog(x) => println(s"It's a $x years old dog.") case Cat(x) => println(s"This cat belongs to $x.") }
Classes de cas et immunité
Le compilateur Scala préfixe chaque argument de la liste de paramètres par défaut avec val
. Cela signifie que, par défaut, les classes de cas sont immuables. Chaque paramètre reçoit une méthode d'accesseur, mais il n'y a pas de méthode de mutation. Par exemple:
case class Foo(i: Int)
val fooInstance = Foo(1)
val j = fooInstance.i // get
fooInstance.i = 2 // compile-time exception (mutation: reassignment to val)
Déclarer un paramètre dans une classe de cas comme var
remplace le comportement par défaut et rend la classe de casable mutable:
case class Bar(var i: Int)
val barInstance = Bar(1)
val j = barInstance.i // get
barInstance.i = 2 // set
Un autre cas où une classe de cas est «mutable» est lorsque la valeur de la classe de casse est modifiable:
import scala.collection._
case class Bar(m: mutable.Map[Int, Int])
val barInstance = Bar(mutable.Map(1 -> 2))
barInstance.m.update(1, 3) // mutate m
barInstance // Bar(Map(1 -> 3)
Notez que la «mutation» qui se produit ici se trouve dans la carte sur laquelle m
pointe, pas sur m
lui-même. Ainsi, si un autre objet avait m
tant que membre, il verrait également le changement. Notez que dans l'exemple suivant, change instanceA
modifie également instanceB
:
import scala.collection.mutable
case class Bar(m: mutable.Map[Int, Int])
val m = mutable.Map(1 ->2)
val barInstanceA = Bar(m)
val barInstanceB = Bar(m)
barInstanceA.m.update(1,3)
barInstanceA // Bar = Bar(Map(1 -> 3))
barInstanceB // Bar = Bar(Map(1 -> 3))
m // scala.collection.mutable.Map[Int,Int] = Map(1 -> 3)
Créer une copie d'un objet avec certaines modifications
Les classes de cas fournissent une méthode de copy
qui crée un nouvel objet qui partage les mêmes champs que l'ancien, avec certaines modifications.
Nous pouvons utiliser cette fonctionnalité pour créer un nouvel objet à partir d'un précédent qui présente certaines des mêmes caractéristiques. Cette classe de cas simple pour démontrer cette fonctionnalité:
case class Person(firstName: String, lastName: String, grade: String, subject: String)
val putu = Person("Putu", "Kevin", "A1", "Math")
val mark = putu.copy(firstName = "Ketut", lastName = "Mark")
// mark: People = People(Ketut,Mark,A1,Math)
Dans cet exemple, nous pouvons voir que les deux objets partagent des caractéristiques similaires ( grade = A1
, subject = Math
), sauf s'ils ont été spécifiés dans la copie ( firstName
et lastName
).
Classes de cas à élément unique pour la sécurité de type
Pour atteindre la sécurité de type, nous voulons parfois éviter l'utilisation de types primitifs sur notre domaine. Par exemple, imaginez une Person
avec un name
. En règle générale, nous coderons le name
tant que String
. Cependant, il ne serait pas difficile de mélanger une String
représentant une Person
de » name
d'une String
représentant un message d'erreur:
def logError(message: ErrorMessage): Unit = ???
case class Person(name: String)
val maybeName: Either[String, String] = ??? // Left is error, Right is name
maybeName.foreach(logError) // But that won't stop me from logging the name as an error!
Pour éviter de tels écueils, vous pouvez encoder les données comme ceci:
case class PersonName(value: String)
case class ErrorMessage(value: String)
case class Person(name: PersonName)
et maintenant notre code ne compilera pas si nous PersonName
avec ErrorMessage
, ou même une String
ordinaire.
val maybeName: Either[ErrorMessage, PersonName] = ???
maybeName.foreach(reportError) // ERROR: tried to pass PersonName; ErrorMessage expected
maybeName.swap.foreach(reportError) // OK
Mais cela entraîne un petit temps d’exécution, car nous devons maintenant cocher / décocher les String
vers / depuis leurs conteneurs PersonName
. Pour éviter cela, on peut créer des classes de valeur PersonName
et ErrorMessage
:
case class PersonName(val value: String) extends AnyVal
case class ErrorMessage(val value: String) extends AnyVal