Qual è la differenza tra la classe maiuscola e la classe di Scala?


440

Ho cercato su Google per trovare le differenze tra a case classe a class. Tutti menzionano che quando si desidera eseguire la corrispondenza dei modelli sulla classe, utilizzare la classe case. Altrimenti usa le classi e menziona anche alcuni vantaggi extra come uguali e la sostituzione del codice hash. Ma questi sono gli unici motivi per cui si dovrebbe usare una classe case anziché una classe?

Immagino che ci dovrebbe essere una ragione molto importante per questa caratteristica in Scala. Qual è la spiegazione o esiste una risorsa per saperne di più sulle classi di casi Scala?

Risposte:


394

Le classi di casi possono essere viste come oggetti di memorizzazione dei dati semplici e immutabili che dovrebbero dipendere esclusivamente dai loro argomenti di costruzione .

Questo concetto funzionale ci consente di farlo

  • usa una sintassi di inizializzazione compatta ( Node(1, Leaf(2), None)))
  • scomporli utilizzando la corrispondenza dei motivi
  • hanno confronti di uguaglianza definiti implicitamente

In combinazione con l'ereditarietà, le classi di casi vengono utilizzate per imitare i tipi di dati algebrici .

Se un oggetto esegue calcoli con stato all'interno o mostra altri tipi di comportamento complesso, dovrebbe essere una classe ordinaria.


11
@Teja: in qualche modo. Gli ADT sono in qualche modo enumerati parametri , estremamente potenti e dattiloscritti.
Dario,

8
Le classi di case sigillate vengono utilizzate per imitare tipi di dati algebrici. Altrimenti il ​​numero di sottoclassi non è limitato.
Thomas Jung,

6
@Thomas: correttamente parlato, le classi di casi derivanti da classi astratte sigillate imitano tipi di dati algebrici chiusi mentre l'ADT è altrimenti aperto .
Dario,

2
@Dario ... e il tipo è altrimenti aperto e non e un ADT. :-)
Thomas Jung,

1
@Thomas: Sì, è solo un esistenziale;)
Dario,

165

Tecnicamente, non c'è alcuna differenza tra una classe e una classe case - anche se il compilatore ottimizza alcune cose quando usa le classi case. Tuttavia, viene utilizzata una classe case per eliminare la piastra della caldaia per un modello specifico, che implementa tipi di dati algebrici .

Un esempio molto semplice di tali tipi sono gli alberi. Un albero binario, ad esempio, può essere implementato in questo modo:

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

Ciò ci consente di fare quanto segue:

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

Si noti che gli alberi costruiscono e decostruiscono (attraverso la corrispondenza del modello) con la stessa sintassi, che è anche esattamente il modo in cui vengono stampati (meno gli spazi).

E possono anche essere usati con mappe o set di hash, poiché hanno un hashCode valido e stabile.


71
  • Le classi di casi possono essere associate al modello
  • Le classi di casi definiscono automaticamente hashcode ed equals
  • Le classi case definiscono automaticamente i metodi getter per gli argomenti del costruttore.

(Hai già menzionato tutto tranne l'ultimo).

Queste sono le uniche differenze rispetto alle lezioni regolari.


13
I setter non vengono generati per le classi case a meno che non sia specificato "var" nell'argomento del costruttore, nel qual caso si ottiene la stessa generazione getter / setter delle classi normali.
Mitch Blevins,

1
@Mitch: vero, mio ​​male. Riparato ora.
sepp2k,

Hai omesso 2 differenze, vedi la mia risposta.
Shelby Moore III,

@MitchBlevins, le classi regolari non hanno sempre la generazione getter / setter.
Shelby Moore III,

Le classi di casi definiscono il metodo di non applicazione ed è per questo che possono essere associate al modello.
Happy Torturer,

30

Nessuno ha menzionato che le classi di casi sono anche istanze di Producte quindi ereditano questi metodi:

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

dove productArityrestituisce il numero di parametri di classe, productElement(i)restituisce l' i esimo parametro e productIteratorconsente di scorrere attraverso di essi.


2
Non sono tuttavia istanze di Product1, Product2, ecc.
Jean-Philippe Pellet,

27

Nessuno ha menzionato che le classi di case hanno valparametri di costruzione, ma questo è anche il valore predefinito per le classi regolari (che penso sia un'incoerenza nella progettazione di Scala). Dario ha insinuato tale dove ha notato che sono " immutabili ".

Nota che puoi sovrascrivere il valore predefinito anteponendo ogni argomento del costruttore con varper le classi case. Tuttavia, rendere mutevoli le classi di casi fa sì che la loro equalse i loro hashCodemetodi siano la variante temporale. [1]

sepp2k ha già menzionato che le classi di casi generano equalse hashCodemetodi automaticamente .

Inoltre, nessuno ha menzionato che le classi di case creano automaticamente un compagno objectcon lo stesso nome della classe, che contiene applye unapplymetodi. Il applymetodo consente di costruire istanze senza anteporre a new. Ilunapply metodo extractor abilita la corrispondenza del modello menzionata da altri.

Inoltre, il compilatore ottimizza la velocità di match- casecorrispondenza del modello per le classi di casi [2].

[1] Le classi di casi sono fantastiche

[2] Classi ed estrattori di casi, pag.15 .


12

Il costrutto della classe case in Scala può anche essere visto come una comodità per rimuovere alcune piastre di caldaia.

Quando costruisci una classe di casi Scala ti offre quanto segue.

  • Crea una classe e l'oggetto associato
  • Il suo oggetto associato implementa il applymetodo che è possibile utilizzare come metodo di fabbrica. Ottieni il vantaggio dello zucchero sintattico di non dover usare la nuova parola chiave.

Poiché la classe è immutabile, si ottengono gli accessor, che sono solo le variabili (o le proprietà) della classe ma nessun mutatore (quindi nessuna possibilità di modificare le variabili). I parametri del costruttore sono automaticamente disponibili come campi di sola lettura pubblici. Molto più bello da usare rispetto al costrutto del bean Java.

  • Vedrete anche hashCode, equalse toStringmetodi per impostazione predefinita e il equalsmetodo confronta un oggetto strutturalmente. Viene copygenerato un metodo per poter clonare un oggetto (con alcuni campi con nuovi valori forniti al metodo).

Il più grande vantaggio, come è stato menzionato in precedenza, è il fatto che è possibile modellare la corrispondenza su classi di casi. Il motivo è dovuto al fatto che si ottiene il unapplymetodo che consente di decostruire una classe case per estrarne i campi.


In sostanza quello che ottieni da Scala quando crei una classe case (o un oggetto case se la tua classe non accetta argomenti) è un oggetto singleton che serve allo scopo come factory e come estrattore .


Perché avresti bisogno di una copia di un oggetto immutabile?
Paŭlo Ebermann,

@ PaŭloEbermann Perché il copymetodo può modificare i campi:val x = y.copy(foo="newValue")
Thilo

8

A parte ciò che la gente ha già detto, ci sono alcune differenze di base tra classecase class

1. Case Classnon ha bisogno di esplicito new, mentre la classe deve essere chiamata connew

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

2.Per i parametri predefiniti dei costruttori sono private in class, mentre il suo public incase 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 classconfrontarsi per valore

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

6

Secondo la documentazione di Scala :

Le classi di casi sono solo classi regolari che sono:

  • Immutabile per impostazione predefinita
  • Scomponibile attraverso la corrispondenza dei motivi
  • Rispetto all'uguaglianza strutturale anziché al riferimento
  • Succinto per istanziare e operare

Un'altra caratteristica della parola chiave case è che il compilatore genera automaticamente diversi metodi per noi, inclusi i metodi familiari toString, equals e hashCode in Java.


5

Classe:

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
           ^

Ma se utilizziamo lo stesso codice ma utilizziamo la classe case:

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

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

Corrispondenza del modello:

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

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

5

Per avere la massima comprensione di ciò che è una classe di casi:

assumiamo la seguente definizione della classe case:

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

e quindi fare quanto segue nel terminale:

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

Scala 2.12.8 produrrà:

...
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>();
    ()
  }
}
...

Come possiamo vedere, il compilatore Scala produce una classe Fooe un oggetto compagno regolari Foo.

Esaminiamo la classe compilata e commentiamo ciò che abbiamo:

  • lo stato interno della Fooclasse, immutabile:
val foo: String
val bar: Int
  • Getters:
def foo(): String
def bar(): Int
  • metodi di copia:
def copy(foo: String, bar: Int): Foo
def copy$default$1(): String
def copy$default$2(): Int
  • scala.Producttratto di attuazione :
override def productPrefix(): String
def productArity(): Int
def productElement(x$1: Int): Object
override def productIterator(): Iterator
  • scala.Equalscaratteristica di implementazione per rendere le istanze della classe case comparabili per l'uguaglianza mediante ==:
def canEqual(x$1: Object): Boolean
override def equals(x$1: Object): Boolean
  • scavalcando java.lang.Object.hashCodeper obbedire al contratto equals-hashcode:
override <synthetic> def hashCode(): Int
  • prevalente java.lang.Object.toString:
override def toString(): String
  • costruttore per l'istanza per newparola chiave:
def <init>(foo: String, bar: Int): Foo 

Oggetto Foo: - metodo applyper l'istanza senza newparola chiave:

case <synthetic> def apply(foo: String, bar: Int): Foo = new Foo(foo, bar);
  • metodo di estrazione unupplyper l'utilizzo della classe di case Foo nella corrispondenza del modello:
case <synthetic> def unapply(x$0: Foo): Option
  • metodo per proteggere l'oggetto come singleton dalla deserializzazione per non lasciare produrre un'altra istanza:
<synthetic> private def readResolve(): Object = Foo;
  • l'oggetto Foo si estende scala.runtime.AbstractFunction2per fare questo trucco:
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 restituisce una funzione per creare un nuovo Foo applicando una tupla di 2 elementi.

Quindi la classe case è solo zucchero sintattico.


4

A differenza delle classi, le classi di casi vengono utilizzate solo per conservare i dati.

Le classi di casi sono flessibili per le applicazioni incentrate sui dati, il che significa che è possibile definire i campi di dati nella classe di casi e definire la logica aziendale in un oggetto associato. In questo modo, si stanno separando i dati dalla logica aziendale.

Con il metodo copia, è possibile ereditare una o tutte le proprietà richieste dall'origine e modificarle a piacere.


3

Nessuno ha menzionato che l'oggetto compagno della classe case ha tupleddifese, che ha un tipo:

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

L'unico caso d'uso che posso trovare è quando devi costruire la classe case dalla tupla, esempio:

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

Puoi fare lo stesso, senza tuple, creando direttamente l'oggetto, ma se i tuoi set di dati espressi come elenco di tuple con arity 20 (tupla con 20 elementi), puoi usare tupled è la tua scelta.


3

Una classe case è una classe che può essere utilizzata con l' match/caseistruzione.

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

Lo vedi case è seguito da un'istanza di classe Fun il cui secondo parametro è un Var. Questa è una sintassi molto bella e potente, ma non può funzionare con istanze di qualsiasi classe, quindi ci sono alcune restrizioni per le classi di casi. E se queste restrizioni vengono rispettate, è possibile definire automaticamente hashcode ed equals.

La vaga frase "un meccanismo di decomposizione ricorsiva tramite pattern matching" significa semplicemente "funziona con case". (In effetti, l'istanza seguita matchviene confrontata con (confrontata con) l'istanza che segue case, Scala deve scomporli entrambi e deve ricomporre ricorsivamente di cosa sono fatti.)

Per quali classi di casi sono utili? L' articolo di Wikipedia sui tipi di dati algebrici fornisce due buoni esempi classici, elenchi e alberi. Il supporto per i tipi di dati algebrici (incluso il saperli confrontare) è un must per qualsiasi linguaggio funzionale moderno.

Per quali classi di casi non sono utili? Alcuni oggetti hanno stato, il codice come connection.setConnectTimeout(connectTimeout)non è per le classi case.

E ora puoi leggere A Tour of Scala: Classi di casi


2

Penso che nel complesso tutte le risposte abbiano dato una spiegazione semantica su classi e classi di casi. Questo potrebbe essere molto rilevante, ma ogni novizio in Scala dovrebbe sapere cosa succede quando si crea una classe di casi. Ho scritto questa risposta, che spiega in breve la classe del caso.

Ogni programmatore dovrebbe sapere che se stanno usando delle funzioni predefinite, allora stanno scrivendo un codice relativamente meno, il che li abilita dando il potere di scrivere il codice più ottimizzato, ma il potere ha grandi responsabilità. Quindi, utilizzare le funzioni predefinite con molta cautela.

Alcuni sviluppatori evitano di scrivere classi di casi a causa di 20 metodi aggiuntivi, che è possibile vedere smontando il file di classe.

Fare riferimento a questo collegamento se si desidera verificare tutti i metodi all'interno di una classe di casi .


1
  • Le classi di casi definiscono un oggetto companionon con metodi apply e unpply
  • Le classi di casi estendono Serializable
  • Le classi di casi definiscono metodi hashCode e copia uguali
  • Tutti gli attributi del costruttore sono val (zucchero sintattico)

1

Alcune delle caratteristiche principali di case classessono elencate di seguito

  1. le classi di casi sono immutabili.
  2. È possibile creare un'istanza di classi di casi senza newparola chiave.
  3. le classi di casi possono essere confrontate per valore

Esempio di codice scala su scala violino, tratto dai documenti scala.

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.