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 pas unapply :

    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


Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow