¿Cuál es la diferencia entre la clase de caso y la clase de Scala?

Busqué en Google para encontrar las diferencias entre una case class y una class . Todo el mundo menciona que cuando desee hacer una coincidencia de patrones en la clase, use la clase de casos. De lo contrario, use las clases y mencione algunas ventajas adicionales, como la anulación de los códigos hash y equals. Pero, ¿son estas las únicas razones por las que uno debería usar una clase de caso en lugar de una clase?

Supongo que debería haber una razón muy importante para esta función en Scala. ¿Cuál es la explicación o hay un recurso para aprender más sobre las clases de casos de Scala?

Las clases de casos se pueden ver como objetos de soporte de datos simples e inmutables que deberían depender exclusivamente de sus argumentos de constructor .

Este concepto funcional nos permite

  • use una syntax de inicialización compacta ( Node(1, Leaf(2), None)) )
  • descomponerlos usando el patrón de coincidencia
  • tener comparaciones de igualdad implícitamente definidas

En combinación con la herencia, las clases de casos se utilizan para imitar los tipos de datos algebraicos .

Si un objeto realiza cálculos con estado en el interior o exhibe otros tipos de comportamiento complejo, debe ser una clase ordinaria.

Técnicamente, no hay diferencia entre una clase y una clase de caso, incluso si el comstackdor optimiza algunas cosas al usar clases de casos. Sin embargo, una clase de caso se utiliza para eliminar la placa de la caldera para un patrón específico, que está implementando tipos de datos algebraicos .

Un ejemplo muy simple de tales tipos son árboles. Un árbol binario, por ejemplo, se puede implementar así:

 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 

Eso nos permite hacer lo siguiente:

 // 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 => } 

Tenga en cuenta que los árboles construyen y deconstruyen (mediante coincidencia de patrones) con la misma syntax, que también es exactamente cómo se imprimen (menos espacios).

Y también se pueden usar con mapas o conjuntos hash, ya que tienen un hashCode válido y estable.

  • Las clases de casos se pueden emparejar
  • Las clases de casos definen automáticamente hashcode e iguales
  • Las clases de casos definen automáticamente los métodos getter para los argumentos del constructor.

(Usted ya mencionó todo menos el último).

Esas son las únicas diferencias con las clases regulares.

Nadie mencionó que las clases de casos también son instancias de Product y, por lo tanto, heredan estos métodos:

 def productElement(n: Int): Any def productArity: Int def productIterator: Iterator[Any] 

donde productArity devuelve el número de parámetros de clase, productElement(i) devuelve el parámetro i th , y productIterator permite iterar a través de ellos.

Nadie mencionó que las clases de casos tienen parámetros de constructor val , pero este también es el predeterminado para las clases regulares (que creo que es una inconsistencia en el diseño de Scala). Darío dio a entender que tal como él señaló que son ” inmutables “.

Tenga en cuenta que puede anular el valor predeterminado anteponiendo el argumento de cada constructor con var para las clases de casos. Sin embargo, hacer que las clases de casos sean mutables hace que sus métodos equals y hashCode sean variantes de tiempo. [1]

sepp2k ya mencionó que las clases de casos generan automáticamente métodos equals y hashCode .

Además, nadie mencionó que las clases de casos crean automáticamente un object complementario con el mismo nombre que la clase, que contiene los métodos apply y unapply . El método apply permite construir instancias sin preceder a las new . El método de extracción sin aplicación permite la coincidencia de patrones que otros mencionaron.

Además, el comstackdor optimiza la velocidad de match patrón de coincidencia de caso para clases de casos [2].

[1] Las clases de casos son geniales

[2] Clases de casos y extractores, página 15 .

La construcción de la clase de caso en Scala también se puede ver como una conveniencia para eliminar algún texto repetitivo.

Al construir una clase de caso, Scala te ofrece lo siguiente.

  • Crea una clase así como su objeto compañero
  • Su objeto complementario implementa el método de apply que puede usar como método de fábrica. Obtiene la ventaja sintáctica del azúcar de no tener que usar la palabra clave nueva.

Debido a que la clase es inmutable, obtienes accesadores, que son solo las variables (o propiedades) de la clase pero no los mutadores (por lo que no hay capacidad para cambiar las variables). Los parámetros del constructor están automáticamente disponibles para usted como campos públicos de solo lectura. Mucho mejor para usar que la construcción de Java Bean.

  • También obtiene hashCode , equals y toString de forma predeterminada y el método equals compara estructuralmente un objeto. Se genera un método de copy para poder clonar un objeto.

La mayor ventaja que se ha mencionado anteriormente es el hecho de que puede coincidir con el patrón en las clases de casos. La razón de esto es porque obtienes el método de no unapply , que te permite deconstruir una clase de caso para extraer sus campos.


En esencia, lo que obtienes de Scala cuando creas una clase de caso (o un objeto de caso si tu clase no toma argumentos) es un objeto singleton que sirve para el propósito como una fábrica y como un extractor .

De acuerdo con la documentación de Scala:

Las clases de casos son solo clases regulares que son:

  • Inmutable por defecto
  • Descomponible a través de la coincidencia de patrones
  • Comparado por la igualdad estructural en lugar de por referencia
  • Sucinta para crear instancias y operar

Otra característica de la palabra clave case es que el comstackdor genera automáticamente varios métodos para nosotros, incluidos los métodos familiares toString, equals y hashCode en Java.

Clase:

 scala> class Animal(name:String) defined class Animal scala> val an1 = new Animal("Padddington") an1: Animal = Animal@748860cc scala> an1.name :14: error: value name is not a member of Animal an1.name ^ 

Pero si usamos el mismo código pero usamos la clase de caso:

 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 

Clase de persona:

 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" :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) 

La coincidencia de patrones:

 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 

objeto: 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 

Nadie mencionó que el objeto compañero de la clase de caso tiene una tupled tupled, que tiene un tipo:

 case class Person(name: String, age: Int) //Person.tupled is def tupled: ((String, Int)) => Person 

El único caso de uso que puedo encontrar es cuando necesitas construir una clase de caso desde una tupla, por ejemplo:

 val bobAsTuple = ("bob", 14) val bob = (Person.apply _).tupled(bobAsTuple) //bob: Person = Person(bob,14) 

Puede hacer lo mismo, sin tuplas, creando objetos directamente, pero si sus conjuntos de datos se expresan como una lista de tuplas con arity 20 (tupla con 20 elementos), puede estar usando tupled es su elección.

Una clase de caso es una clase que se puede usar con la statement de match/case .

 def isIdentityFun(term: Term): Boolean = term match { case Fun(x, Var(y)) if x == y => true case _ => false } 

Verá que ese case es seguido por una instancia de la clase Fun cuyo segundo parámetro es un Var. Esta es una syntax muy buena y poderosa, pero no puede funcionar con instancias de ninguna clase, por lo tanto, hay algunas restricciones para las clases de casos. Y si se obedecen estas restricciones, es posible definir automáticamente hashcode y equals.

La vaga frase “un mecanismo recursivo de descomposición a través de la coincidencia de patrones” significa simplemente “funciona con el case “. (De hecho, la instancia seguida por match se compara con (se compara con) la instancia que sigue al case , Scala tiene que descomponerlos a ambos, y tiene que descomponer recursivamente de lo que están hechos).

¿Para qué clases de casos son útiles? El artículo de Wikipedia sobre tipos de datos algebraicos ofrece dos buenos ejemplos clásicos, listas y árboles. El soporte para tipos de datos algebraicos (incluido saber cómo compararlos) es imprescindible para cualquier lenguaje funcional moderno.

¿Qué clases de casos no son útiles? Algunos objetos tienen estado, el código como connection.setConnectTimeout(connectTimeout) no es para clases de casos.

Y ahora puedes leer Un recorrido por Scala: Clases de casos

Además de lo que la gente ya ha dicho, existen algunas diferencias más básicas entre class y case class

1. Case Class no necesita new explícito, mientras que la clase necesita ser llamada con new

 val classInst = new MyClass(...) // For classes val classInst = MyClass(..) // For case class 

2. Por defecto los parámetros de los constructores son privados en class , mientras que su público en la 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 comparan por valor

 // 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 

A diferencia de las clases, las clases de casos solo se usan para guardar datos.

Las clases de casos son flexibles para aplicaciones centradas en datos, lo que significa que puede definir campos de datos en la clase de caso y definir la lógica de negocios en un objeto complementario. De esta forma, está separando los datos de la lógica comercial.

Con el método de copia, puede heredar cualquiera o todas las propiedades requeridas de la fuente y puede cambiarlas a su gusto.

  • Las clases de casos definen un objeto compagnon con métodos apply y unpply
  • Las clases de casos se extienden a Serializable
  • Las clases de casos definen iguales hashCode y copian métodos
  • Todos los atributos del constructor son val (azúcar sintáctico)

Creo que, en general, todas las respuestas dieron una explicación semántica sobre las clases y las clases de casos. Esto podría ser muy relevante, pero cada novato en scala debe saber qué sucede cuando creas una clase de caso. He escrito esta respuesta, que explica la clase de caso en pocas palabras.

Todos los progtwigdores deben saber que si están utilizando funciones preconstruidas, entonces están escribiendo un código comparativamente menor, lo que les permite dar la potencia para escribir el código más optimizado, pero el poder conlleva grandes responsabilidades. Por lo tanto, use funciones preconstruidas con mucha precaución.

Algunos desarrolladores evitan escribir clases de casos debido a 20 métodos adicionales, que puede ver al desmontar el archivo de clase.

Consulte este enlace si desea verificar todos los métodos dentro de una clase de caso .