Buscar..


Sintaxis

  • clase de caso Foo () // Las clases de caso sin parámetros deben tener una lista vacía
  • clase de caso Foo (a1: A1, ..., aN: AN) // Crear una clase de caso con los campos a1 ... aN
  • objeto case Bar // Crear una clase de caso singleton

Igualdad de clase de caso

Una característica proporcionada de forma gratuita por clases de casos es un método de generación automática de equals que verifica la igualdad de valores de todos los campos de miembros individuales en lugar de solo verificar la igualdad de referencia de los objetos.

Con las clases ordinarias:

class Foo(val i: Int)
val a = new Foo(3)
val b = new Foo(3)
println(a == b)// "false" because they are different objects

Con clases de casos:

case class Foo(i: Int)
val a = Foo(3)
val b = Foo(3)
println(a == b)// "true" because their members have the same value

Códigos generados

El modificador de case hace que el compilador de Scala genere automáticamente un código común para la clase. Implementar este código manualmente es tedioso y una fuente de errores. La siguiente definición de clase de caso:

case class Person(name: String, age: Int)

... se generará automáticamente el siguiente código:

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)
}

El modificador de case también genera un objeto complementario:

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))
}

Cuando se aplica a un object , el modificador de case tiene efectos similares (aunque menos dramáticos). Aquí las principales ganancias son una implementación de toString y un valor de hashCode que es consistente en todos los procesos. Tenga en cuenta que los objetos de caso (correctamente) utilizan la igualdad de referencia:

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"
}

Todavía es posible implementar manualmente métodos que de otra manera serían proporcionados por el modificador de case tanto en la clase en sí como en su objeto complementario.

Fundamentos de clase de caso

En comparación con las clases regulares, la notación de clases de casos brinda varios beneficios:

  • Todos los argumentos del constructor son public y se puede acceder a ellos en objetos inicializados (normalmente este no es el caso, como se muestra aquí):

    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"
    
  • Proporciona una implementación para los siguientes métodos: toString , equals , hashCode (basado en propiedades), copy , apply y no unapply :

    case class Dog(age: Int)
    val d1 = Dog(10)
    val d2 = d1.copy(age = 15)
    
  • Proporciona un mecanismo conveniente para la coincidencia de patrones:

    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.")
    }
    

Clases de casos e inmutabilidad

El compilador de Scala prefija cada argumento en la lista de parámetros por defecto con val . Esto significa que, por defecto, las clases de casos son inmutables. Cada parámetro recibe un método de acceso, pero no hay métodos mutadores. Por ejemplo:

case class Foo(i: Int)

val fooInstance = Foo(1)
val j = fooInstance.i       // get
fooInstance.i = 2           // compile-time exception (mutation: reassignment to val)

Declarar un parámetro en una clase de caso como var anula el comportamiento predeterminado y hace que la clase de caso sea mutable:

case class Bar(var i: Int)

val barInstance = Bar(1)
val j = barInstance.i       // get
barInstance.i = 2           // set

Otra instancia cuando una clase de caso es 'mutable' es cuando el valor en la clase de caso es mutable:

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)

Tenga en cuenta que la 'mutación' que está ocurriendo aquí está en el mapa que m apunta, no m sí. Por lo tanto, si algún otro objeto tuviera m como miembro, vería el cambio también. Observe cómo en el siguiente ejemplo, cambiar la instanceA también cambia la 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)

Crear una copia de un objeto con ciertos cambios

Las clases de casos proporcionan un método de copy que crea un nuevo objeto que comparte los mismos campos que el anterior, con ciertos cambios.

Podemos usar esta función para crear un nuevo objeto a partir de uno anterior que tenga algunas de las mismas características. Esta clase de caso simple demuestra esta característica:

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)

En este ejemplo se puede ver que los dos objetos comparten características similares ( grade = A1 , subject = Math ), salvo que se han especificado en la copia ( firstName y lastName ).

Clases de caso de un solo elemento para seguridad de tipos

Para lograr la seguridad de tipos, a veces queremos evitar el uso de tipos primitivos en nuestro dominio. Por ejemplo, imagine una Person con un name . Típicamente, codificaríamos el name como una String . Sin embargo, no sería difícil de mezclar una String que representa una Person 's name con una String que representa un mensaje de error:

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!

Para evitar tales riesgos, puede codificar los datos de esta manera:

case class PersonName(value: String)
case class ErrorMessage(value: String)
case class Person(name: PersonName)

y ahora nuestro código no se compilará si mezclamos PersonName con ErrorMessage , o incluso una String normal.

val maybeName: Either[ErrorMessage, PersonName] = ???
maybeName.foreach(reportError) // ERROR: tried to pass PersonName; ErrorMessage expected
maybeName.swap.foreach(reportError) // OK

Pero esto conlleva una pequeña sobrecarga en el tiempo de ejecución, ya que ahora tenemos que encuadrar / desempaquetar String a / desde sus contenedores PersonName . Para evitar esto, uno puede hacer que las clases de valores PersonName y 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
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow