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

439

Busqué en Google para encontrar las diferencias entre a case classy a class. Todos mencionan que cuando desea hacer una coincidencia de patrones en la clase, use la clase de caso. De lo contrario, use clases y también mencione algunas ventajas adicionales como equals y anulación de código hash. ¿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 alguna razón muy importante para esta característica en Scala. ¿Cuál es la explicación o hay un recurso para aprender más sobre las clases de casos Scala?

Teja Kantamneni
fuente

Respuestas:

393

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

Este concepto funcional nos permite

  • usar una sintaxis de inicialización compacta ( Node(1, Leaf(2), None)))
  • descomponerlos utilizando la coincidencia de patrones
  • tener comparaciones de igualdad definidas implícitamente

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

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

Darío
fuente
11
@Teja: De alguna manera. Los ADT son enumeraciones un poco parametrizadas , extremadamente potentes y seguros.
Dario
8
Las clases de casos selladas se utilizan para imitar tipos de datos algebraicos. De lo contrario, el número de subclases no está limitado.
Thomas Jung
66
@Thomas: correctamente dicho, las clases de casos que derivan de clases abstractas selladas imitan tipos de datos algebraicos cerrados, mientras que el ADT está abierto de otra manera .
Dario
2
@Dario ... y el tipo está abierto y no ADT. :-)
Thomas Jung
1
@Thomas: Sí, es simplemente un existencial;)
Dario
165

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

Un ejemplo muy simple de tales tipos son los árboles. Un árbol binario, por ejemplo, puede implementarse 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 (a través de la coincidencia de patrones) con la misma sintaxis, que también es exactamente cómo se imprimen (menos espacios).

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

Daniel C. Sobral
fuente
71
  • Las clases de caso se pueden combinar con patrones
  • Las clases de caso definen automáticamente el código hash y es igual a
  • Las clases de casos definen automáticamente los métodos getter para los argumentos del constructor.

(Ya mencionaste todo menos el último).

Esas son las únicas diferencias con las clases regulares.

sepp2k
fuente
13
Los establecedores no se generan para las clases de caso a menos que se especifique "var" en el argumento del constructor, en cuyo caso se obtiene la misma generación de captador / definidor que las clases regulares.
Mitch Blevins
1
@ Mitch: Cierto, mi mal. Corregido ahora.
sepp2k
Omitiste 2 diferencias, mira mi respuesta.
Shelby Moore III
@MitchBlevins, las clases regulares no siempre tienen generación getter / setter.
Shelby Moore III
Las clases de casos definen el método de no aplicación, por eso se pueden combinar patrones.
Happy Torturer
30

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

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

donde productAritydevuelve el número de parámetros de clase, productElement(i)devuelve el i ésimo parámetro y productIteratorpermite iterar a través de ellos.

Jean-Philippe Pellet
fuente
2
Sin embargo, no son instancias de Producto1, Producto2, etc.
Jean-Philippe Pellet el
27

Nadie mencionó que las clases de caso tienen valparámetros de constructor, pero este también es el valor predeterminado para las clases regulares (lo que creo que es una inconsistencia en el diseño de Scala). Darío dio a entender tal cosa donde notó que son " inmutables ".

Tenga en cuenta que puede anular el valor predeterminado al anteponer el argumento de cada constructor varpara las clases de caso. Sin embargo, hacer clases de casos mutable hace que sus equalsy hashCodemétodos para ser variable en el tiempo. [1]

sepp2k ya mencionó que las clases de casos generan equalsy hashCodemétodos automáticamente .

Además, nadie mencionó que las clases de casos crean automáticamente un compañero objectcon el mismo nombre que la clase, que contiene applyy unapplymétodos. El applymétodo permite construir instancias sin anteponer new. El unapplymétodo extractor permite la coincidencia de patrones que otros mencionaron.

También el compilador optimiza la velocidad de match- casecoincidencia de patrones para las clases de casos [2].

[1] Las clases de casos son geniales

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

Shelby Moore III
fuente
12

La construcción de la clase de caso en Scala también se puede ver como una conveniencia para eliminar algunas repeticiones.

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

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

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

  • También obtiene hashCode, equalsy toStringmétodos por defecto y el equalsmétodo compara estructuralmente un objeto. Se copygenera un método para poder clonar un objeto (con algunos campos que tienen nuevos valores proporcionados al método).

La mayor ventaja, como se mencionó anteriormente, es el hecho de que puede emparejar patrones en las clases de casos. La razón de esto es porque obtienes el unapplymétodo 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 como fábrica y como extractor .

Faktor 10
fuente
¿Por qué necesitarías una copia de un objeto inmutable?
Paŭlo Ebermann
@ PaŭloEbermann Debido a que el copymétodo puede modificar los campos:val x = y.copy(foo="newValue")
Thilo
8

Además de lo que la gente ya ha dicho, hay algunas diferencias más básicas entre classycase class

1. Case Classno necesita explícito new, mientras que la clase debe llamarse connew

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

2. Por los parámetros predeterminados de los constructores son privados en class, mientras que su público encase 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 classcompararse 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
DeepakKg
fuente
6

Según 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 igualdad estructural en lugar de por referencia
  • Breve para crear instancias y operar

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

Philipp Claßen
fuente
5

Clase:

scala> class Animal(name:String)
defined class Animal

scala> val an1 = new Animal("Padddington")
an1: Animal = Animal@748860cc

scala> an1.name
<console>: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"
<console>: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
usuario1668782
fuente
5

Para tener la mejor comprensión de lo que es una clase de caso:

supongamos la siguiente definición de clase de caso:

case class Foo(foo:String, bar: Int)

y luego haz lo siguiente en la terminal:

$ scalac -print src/main/scala/Foo.scala

Scala 2.12.8 generará:

...
case class Foo extends Object with Product with Serializable {

  <caseaccessor> <paramaccessor> private[this] val foo: String = _;

  <stable> <caseaccessor> <accessor> <paramaccessor> def foo(): String = Foo.this.foo;

  <caseaccessor> <paramaccessor> private[this] val bar: Int = _;

  <stable> <caseaccessor> <accessor> <paramaccessor> def bar(): Int = Foo.this.bar;

  <synthetic> def copy(foo: String, bar: Int): Foo = new Foo(foo, bar);

  <synthetic> def copy$default$1(): String = Foo.this.foo();

  <synthetic> def copy$default$2(): Int = Foo.this.bar();

  override <synthetic> def productPrefix(): String = "Foo";

  <synthetic> def productArity(): Int = 2;

  <synthetic> def productElement(x$1: Int): Object = {
    case <synthetic> val x1: Int = x$1;
        (x1: Int) match {
            case 0 => Foo.this.foo()
            case 1 => scala.Int.box(Foo.this.bar())
            case _ => throw new IndexOutOfBoundsException(scala.Int.box(x$1).toString())
        }
  };

  override <synthetic> def productIterator(): Iterator = scala.runtime.ScalaRunTime.typedProductIterator(Foo.this);

  <synthetic> def canEqual(x$1: Object): Boolean = x$1.$isInstanceOf[Foo]();

  override <synthetic> def hashCode(): Int = {
     <synthetic> var acc: Int = -889275714;
     acc = scala.runtime.Statics.mix(acc, scala.runtime.Statics.anyHash(Foo.this.foo()));
     acc = scala.runtime.Statics.mix(acc, Foo.this.bar());
     scala.runtime.Statics.finalizeHash(acc, 2)
  };

  override <synthetic> def toString(): String = scala.runtime.ScalaRunTime._toString(Foo.this);

  override <synthetic> def equals(x$1: Object): Boolean = Foo.this.eq(x$1).||({
      case <synthetic> val x1: Object = x$1;
        case5(){
          if (x1.$isInstanceOf[Foo]())
            matchEnd4(true)
          else
            case6()
        };
        case6(){
          matchEnd4(false)
        };
        matchEnd4(x: Boolean){
          x
        }
    }.&&({
      <synthetic> val Foo$1: Foo = x$1.$asInstanceOf[Foo]();
      Foo.this.foo().==(Foo$1.foo()).&&(Foo.this.bar().==(Foo$1.bar())).&&(Foo$1.canEqual(Foo.this))
  }));

  def <init>(foo: String, bar: Int): Foo = {
    Foo.this.foo = foo;
    Foo.this.bar = bar;
    Foo.super.<init>();
    Foo.super./*Product*/$init$();
    ()
  }
};

<synthetic> object Foo extends scala.runtime.AbstractFunction2 with Serializable {

  final override <synthetic> def toString(): String = "Foo";

  case <synthetic> def apply(foo: String, bar: Int): Foo = new Foo(foo, bar);

  case <synthetic> def unapply(x$0: Foo): Option =
     if (x$0.==(null))
        scala.None
     else
        new Some(new Tuple2(x$0.foo(), scala.Int.box(x$0.bar())));

  <synthetic> private def readResolve(): Object = Foo;

  case <synthetic> <bridge> <artifact> def apply(v1: Object, v2: Object): Object = Foo.this.apply(v1.$asInstanceOf[String](), scala.Int.unbox(v2));

  def <init>(): Foo.type = {
    Foo.super.<init>();
    ()
  }
}
...

Como podemos ver, el compilador de Scala produce una clase regular Fooy un objeto complementario Foo.

Repasemos la clase compilada y comentemos lo que tenemos:

  • El estado interno de la Fooclase, inmutable:
val foo: String
val bar: Int
  • captadores:
def foo(): String
def bar(): Int
  • métodos de copia:
def copy(foo: String, bar: Int): Foo
def copy$default$1(): String
def copy$default$2(): Int
  • scala.Productrasgo de implementación :
override def productPrefix(): String
def productArity(): Int
def productElement(x$1: Int): Object
override def productIterator(): Iterator
  • scala.Equalsrasgo de implementación para hacer instancias de clase de caso comparables para la igualdad mediante ==:
def canEqual(x$1: Object): Boolean
override def equals(x$1: Object): Boolean
  • anulación java.lang.Object.hashCodepor obedecer el contrato equals-hashcode:
override <synthetic> def hashCode(): Int
  • anulación java.lang.Object.toString:
override def toString(): String
  • constructor para instanciación por newpalabra clave:
def <init>(foo: String, bar: Int): Foo 

Object Foo: - método applypara instanciación sin newpalabra clave:

case <synthetic> def apply(foo: String, bar: Int): Foo = new Foo(foo, bar);
  • Método de extracción unupplypara utilizar la clase de caso Foo en la coincidencia de patrones:
case <synthetic> def unapply(x$0: Foo): Option
  • Método para proteger el objeto como singleton de la deserialización por no dejar que produzca una instancia más:
<synthetic> private def readResolve(): Object = Foo;
  • objeto Foo se extiende scala.runtime.AbstractFunction2por hacer tal truco:
scala> case class Foo(foo:String, bar: Int)
defined class Foo

scala> Foo.tupled
res1: ((String, Int)) => Foo = scala.Function2$$Lambda$224/1935637221@9ab310b

tupled from object devuelve una función para crear un nuevo Foo aplicando una tupla de 2 elementos.

Entonces, la clase de caso es solo azúcar sintáctico.

Mateo I.
fuente
4

A diferencia de las clases, las clases de casos solo se utilizan para contener 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 casos y definir la lógica de negocios en un objeto complementario. De esta manera, está separando los datos de la lógica empresarial.

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

Reddeiah Pidugu
fuente
3

Nadie mencionó que el objeto complementario de la clase de caso tiene tupleddefention, 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 a partir de una tupla, por ejemplo:

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

Puede hacer lo mismo, sin tupled, creando un objeto directamente, pero si sus conjuntos de datos expresados ​​como una lista de tuple con arity 20 (tuple con 20 elementos), puede usar tupled es su elección.

Harry lento
fuente
3

Una clase de caso es una clase que se puede usar con la match/casedeclaración.

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

Ves eso case es seguido por una instancia de clase Fun cuyo segundo parámetro es un Var. Esta es una sintaxis muy buena y poderosa, pero no puede funcionar con instancias de ninguna clase, por lo tanto, existen algunas restricciones para las clases de casos. Y si se obedecen estas restricciones, es posible definir automáticamente hashcode e iguales.

La vaga frase "un mecanismo de descomposición recursiva mediante la coincidencia de patrones" significa simplemente "funciona con case". (De hecho, la instancia seguida por matchse compara con (se compara con) la instancia que sigue case, Scala tiene que descomponerlos a ambos, y tiene que descomponer recursivamente de qué 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. La compatibilidad con los tipos de datos algebraicos (incluido saber cómo compararlos) es imprescindible para cualquier lenguaje funcional moderno.

¿Para qué clases de caso 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

18446744073709551615
fuente
2

Creo que, en general, todas las respuestas han dado 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 crea una clase de caso. He escrito esta respuesta, que explica la clase de caso en pocas palabras.

Todos los programadores deben saber que si están utilizando funciones predefinidas, entonces están escribiendo un código comparativamente menor, lo que les permite dar el poder de 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 .

arglee
fuente
1
  • Las clases de caso definen un objeto de comparación con los métodos de aplicación y no aplicación
  • Las clases de casos se extienden Serializable
  • Las clases de caso definen igual a hashCode y métodos de copia
  • Todos los atributos del constructor son val (azúcar sintáctico)
tictactoki
fuente
1

Algunas de las características clave de case classesse enumeran a continuación

  1. Las clases de caso son inmutables.
  2. Puede crear instancias de clases de caso sin newpalabra clave.
  3. las clases de casos se pueden comparar por valor

Ejemplo de código scala en el violín scala, tomado de los documentos scala.

https://scalafiddle.io/sf/34XEQyE/0

Krishnadas PC
fuente