Scala Language
Clases de casos
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 nounapply
: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