Was ist der Unterschied zwischen Scalas Fallklasse und Klasse?


439

Ich habe in Google nach den Unterschieden zwischen a case classund a gesucht class. Jeder erwähnt, dass Sie die Fallklasse verwenden, wenn Sie einen Mustervergleich für die Klasse durchführen möchten. Verwenden Sie andernfalls Klassen und erwähnen Sie einige zusätzliche Vorteile wie Gleichheit und Überschreiben von Hash-Code. Aber sind dies die einzigen Gründe, warum man eine Fallklasse anstelle einer Klasse verwenden sollte?

Ich denke, es sollte einen sehr wichtigen Grund für diese Funktion in Scala geben. Was ist die Erklärung oder gibt es eine Ressource, um mehr über die Scala-Fallklassen zu erfahren?

Antworten:


393

Fallklassen können als einfache und unveränderliche datenhaltige Objekte angesehen werden, die ausschließlich von ihren Konstruktorargumenten abhängen sollten .

Dieses Funktionskonzept ermöglicht es uns

  • Verwenden Sie eine kompakte Initialisierungssyntax ( Node(1, Leaf(2), None)))
  • Zerlegen Sie sie mithilfe des Mustervergleichs
  • Gleichheitsvergleiche implizit definiert haben

In Kombination mit der Vererbung werden Fallklassen verwendet, um algebraische Datentypen nachzuahmen .

Wenn ein Objekt im Inneren zustandsbehaftete Berechnungen durchführt oder andere komplexe Verhaltensweisen aufweist, sollte es eine gewöhnliche Klasse sein.


11
@ Teja: In gewisser Weise. ADTs sind irgendwie parametrisierte Aufzählungen , extrem leistungsfähig und typsicher.
Dario

8
Versiegelte Fallklassen werden verwendet, um algebraische Datentypen nachzuahmen. Ansonsten ist die Anzahl der Unterklassen nicht begrenzt.
Thomas Jung

6
@Thomas: Richtig gesprochen ahmen Fallklassen, die von versiegelten abstrakten Klassen stammen, geschlossene algebraische Datentypen nach, während das ADT ansonsten offen ist .
Dario

2
@Dario ... und der Typ ist sonst offen und nicht und ein ADT. :-)
Thomas Jung

1
@ Thomas: Ja, es ist nur eine existenzielle;)
Dario

165

Technisch gesehen gibt es keinen Unterschied zwischen einer Klasse und einer Fallklasse - selbst wenn der Compiler bei der Verwendung von Fallklassen einige Dinge optimiert. Eine Fallklasse wird jedoch verwendet, um die Kesselplatte für ein bestimmtes Muster zu beseitigen, das algebraische Datentypen implementiert .

Ein sehr einfaches Beispiel für solche Typen sind Bäume. Ein Binärbaum kann beispielsweise folgendermaßen implementiert werden:

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

Damit können wir Folgendes tun:

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

Beachten Sie, dass Bäume mit derselben Syntax konstruieren und dekonstruieren (durch Musterübereinstimmung). Dies ist auch genau die Art und Weise, wie sie gedruckt werden (minus Leerzeichen).

Sie können auch mit Hash-Maps oder -Sätzen verwendet werden, da sie einen gültigen, stabilen Hash-Code haben.


71
  • Fallklassen können musterangepasst werden
  • Fallklassen definieren automatisch Hashcode und sind gleich
  • Fallklassen definieren automatisch Getter-Methoden für die Konstruktorargumente.

(Sie haben bereits alle bis auf den letzten erwähnt).

Dies sind die einzigen Unterschiede zu regulären Klassen.


13
Setter werden für Fallklassen nur generiert, wenn im Konstruktorargument "var" angegeben ist. In diesem Fall erhalten Sie dieselbe Getter / Setter-Generierung wie reguläre Klassen.
Mitch Blevins

1
@Mitch: Stimmt, mein schlechtes. Jetzt behoben.
sepp2k

Sie haben 2 Unterschiede ausgelassen, siehe meine Antwort.
Shelby Moore III

@MitchBlevins, reguläre Klassen haben nicht immer eine Getter / Setter-Generierung.
Shelby Moore III

Fallklassen definieren die Methode der Nichtanwendung, weshalb sie mit Mustern abgeglichen werden können.
Glücklicher Folterer

30

Niemand erwähnte, dass Fallklassen auch Instanzen Productdieser Methoden sind und diese daher erben:

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

Dabei productAritygibt der die Anzahl der Klassenparameter zurück, productElement(i)gibt den i- ten Parameter zurück und productIteratorermöglicht das Durchlaufen dieser Parameter .


2
Sie sind jedoch keine Instanzen von Produkt1, Produkt2 usw.
Jean-Philippe Pellet

27

Niemand erwähnte, dass Fallklassen valKonstruktorparameter haben, dies ist jedoch auch die Standardeinstellung für reguläre Klassen (was meiner Meinung nach eine Inkonsistenz im Design von Scala darstellt). Dario implizierte dies, wo er feststellte, dass sie " unveränderlich " sind.

Beachten Sie, dass Sie die Standardeinstellung überschreiben können, indem Sie jedem Konstruktorargument varfor case-Klassen voranstellen . Wenn Fallklassen jedoch veränderlich gemacht werden, sind ihre equalsund hashCodeMethoden zeitlich variabel. [1]

sepp2k bereits erwähnt , dass Fallklassen automatisch erzeugen equalsund hashCodeMethoden.

Außerdem erwähnte niemand, dass Fallklassen automatisch einen Begleiter objectmit demselben Namen wie die Klasse erstellen , die applyund unapplyMethoden enthält . Die applyMethode ermöglicht das Erstellen von Instanzen ohne Voranstellen mit new. Die unapplyExtraktionsmethode ermöglicht den Mustervergleich, den andere erwähnt haben.

Auch der Compiler optimiert die Geschwindigkeit match- caseMustervergleich für Fallklassen [2].

[1] Fallklassen sind cool

[2] Fallklassen und Extraktoren, S. 15 .


12

Das Fallklassenkonstrukt in Scala kann auch als Annehmlichkeit zum Entfernen von Boilerplate angesehen werden.

Beim Erstellen einer Fallklasse bietet Scala Folgendes.

  • Es erstellt eine Klasse sowie ihr Begleitobjekt
  • Das Begleitobjekt implementiert die applyMethode, die Sie als Factory-Methode verwenden können. Sie erhalten den syntaktischen Zuckervorteil, dass Sie das neue Schlüsselwort nicht verwenden müssen.

Da die Klasse unveränderlich ist, erhalten Sie Accessoren, die nur die Variablen (oder Eigenschaften) der Klasse sind, aber keine Mutatoren (also keine Möglichkeit, die Variablen zu ändern). Die Konstruktorparameter stehen Ihnen automatisch als öffentliche schreibgeschützte Felder zur Verfügung. Viel schöner zu verwenden als Java Bean Konstrukt.

  • Sie erhalten auch hashCode, equalsund toStringMethoden standardmäßig aktiviert und das equalsVerfahren vergleicht ein strukturell - Objekt. Eine copyMethode wird generiert, um ein Objekt klonen zu können (wobei einige Felder neue Werte für die Methode bereitstellen).

Der größte Vorteil, wie bereits erwähnt, ist die Tatsache, dass Sie Musterübereinstimmungen für Fallklassen durchführen können. Der Grund dafür ist, dass Sie die unapplyMethode erhalten, mit der Sie eine Fallklasse dekonstruieren können, um ihre Felder zu extrahieren.


Im Wesentlichen erhalten Sie von Scala beim Erstellen einer Fallklasse (oder eines Fallobjekts, wenn Ihre Klasse keine Argumente akzeptiert) ein Singleton-Objekt, das als Factory und als Extraktor dient .


Warum benötigen Sie eine Kopie eines unveränderlichen Objekts?
Paŭlo Ebermann

@ PaŭloEbermann Weil die copyMethode die Felder ändern kann:val x = y.copy(foo="newValue")
Thilo

8

Abgesehen von dem, was die Leute bereits gesagt haben, gibt es einige grundlegendere Unterschiede zwischen classundcase class

1. Case Classmuss nicht explizit sein new, während class mit aufgerufen werden mussnew

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

2.By Standardkonstruktorparameter sind privat in class, während seine öffentlich 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 classVergleichen Sie sich nach Wert

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

Laut Scalas Dokumentation :

Fallklassen sind nur reguläre Klassen, die:

  • Standardmäßig unveränderlich
  • Durch Mustervergleich zerlegbar
  • Verglichen mit struktureller Gleichheit statt mit Referenz
  • Prägnant zu instanziieren und zu bearbeiten

Ein weiteres Merkmal des Schlüsselworts case ist, dass der Compiler automatisch mehrere Methoden für uns generiert, einschließlich der bekannten Methoden toString, equals und hashCode in Java.


5

Klasse:

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
           ^

Wenn wir jedoch denselben Code verwenden, aber die Fallklasse verwenden:

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

Personenklasse:

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)

Mustervergleich:

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

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

Um das ultimative Verständnis für eine Fallklasse zu haben:

Nehmen wir die folgende Definition der Fallklasse an:

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

und machen Sie dann im Terminal Folgendes:

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

Scala 2.12.8 gibt Folgendes aus:

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

Wie wir sehen können, erzeugt der Scala-Compiler eine reguläre Klasse Foound ein Begleitobjekt Foo.

Lassen Sie uns die kompilierte Klasse durchgehen und kommentieren, was wir haben:

  • der innere Zustand der FooKlasse, unveränderlich:
val foo: String
val bar: Int
  • Getter:
def foo(): String
def bar(): Int
  • Kopiermethoden:
def copy(foo: String, bar: Int): Foo
def copy$default$1(): String
def copy$default$2(): Int
  • Implementierungsmerkmal scala.Product:
override def productPrefix(): String
def productArity(): Int
def productElement(x$1: Int): Object
override def productIterator(): Iterator
  • Implementieren eines scala.EqualsMerkmals, um Fallklasseninstanzen für Gleichheit vergleichbar zu machen, indem ==:
def canEqual(x$1: Object): Boolean
override def equals(x$1: Object): Boolean
  • Überschreiben java.lang.Object.hashCodefür die Einhaltung des Equals-Hashcode-Vertrags:
override <synthetic> def hashCode(): Int
  • Überschreiben java.lang.Object.toString:
override def toString(): String
  • Konstruktor für die Instanziierung nach newSchlüsselwort:
def <init>(foo: String, bar: Int): Foo 

Objekt Foo: - Methode applyzur Instanziierung ohne newSchlüsselwort:

case <synthetic> def apply(foo: String, bar: Int): Foo = new Foo(foo, bar);
  • Extraktionsmethode unupplyzur Verwendung der Fallklasse Foo beim Mustervergleich:
case <synthetic> def unapply(x$0: Foo): Option
  • Methode zum Schutz des Objekts als Singleton vor Deserialisierung, damit keine weitere Instanz erzeugt wird:
<synthetic> private def readResolve(): Object = Foo;
  • Objekt Foo erweitert scala.runtime.AbstractFunction2für einen solchen Trick:
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 gibt eine Funktion zurück, um ein neues Foo zu erstellen, indem ein Tupel aus 2 Elementen angewendet wird.

Die Fallklasse ist also nur syntaktischer Zucker.


4

Im Gegensatz zu Klassen werden Fallklassen nur zum Speichern von Daten verwendet.

Fallklassen sind für datenzentrierte Anwendungen flexibel. Dies bedeutet, dass Sie Datenfelder in Fallklassen definieren und Geschäftslogik in einem Begleitobjekt definieren können. Auf diese Weise trennen Sie die Daten von der Geschäftslogik.

Mit der Kopiermethode können Sie einige oder alle erforderlichen Eigenschaften von der Quelle erben und nach Belieben ändern.


3

Niemand erwähnte, dass das Begleitobjekt der tupledFallklasse eine Verteidigung hat, die einen Typ hat:

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

Der einzige Anwendungsfall, den ich finden kann, ist, wenn Sie eine Fallklasse aus einem Tupel erstellen müssen. Beispiel:

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

Sie können dasselbe ohne Tupel tun, indem Sie ein Objekt direkt erstellen. Wenn Ihre Datensätze jedoch als Liste von Tupeln mit Arität 20 (Tupel mit 20 Elementen) ausgedrückt werden, können Sie Tupel verwenden.


3

Eine Fallklasse ist eine Klasse, die mit der match/caseAnweisung verwendet werden kann.

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

Siehst du das case eine Instanz der Klasse Fun folgt, deren zweiter Parameter ein Var ist. Dies ist eine sehr schöne und leistungsstarke Syntax, die jedoch nicht mit Instanzen einer Klasse funktioniert. Daher gibt es einige Einschränkungen für Fallklassen. Und wenn diese Einschränkungen eingehalten werden, ist es möglich, automatisch Hashcode und Gleichheit zu definieren.

Der vage Ausdruck "ein rekursiver Zerlegungsmechanismus über Mustervergleich" bedeutet nur "es funktioniert mit case". (In der Tat wird die folgende Instanz mit der folgenden Instanz matchverglichen (verglichen mit dieser). caseScala muss beide zerlegen und rekursiv zerlegen, woraus sie bestehen.)

Für welche Fallklassen sind nützlich? Der Wikipedia-Artikel über algebraische Datentypen enthält zwei gute klassische Beispiele: Listen und Bäume. Die Unterstützung algebraischer Datentypen (einschließlich des Vergleichs) ist ein Muss für jede moderne Funktionssprache.

Für welche Fallklassen sind sie nicht nützlich? Einige Objekte haben Status, der Code connection.setConnectTimeout(connectTimeout)ist nicht für Fallklassen.

Und jetzt können Sie A Tour of Scala: Fallklassen lesen


2

Ich denke, insgesamt haben alle Antworten eine semantische Erklärung zu Klassen und Fallklassen gegeben. Dies könnte sehr relevant sein, aber jeder Neuling in Scala sollte wissen, was passiert, wenn Sie eine Fallklasse erstellen. Ich habe geschrieben das Antwort geschrieben, die die Fallklasse auf den Punkt bringt.

Jeder Programmierer sollte wissen, dass er, wenn er vorgefertigte Funktionen verwendet, einen vergleichsweise weniger Code schreibt, was es ihm ermöglicht, den optimiertesten Code zu schreiben, aber die Leistung bringt große Verantwortung mit sich. Verwenden Sie daher vorgefertigte Funktionen mit sehr Vorsicht.

Einige Entwickler vermeiden das Schreiben von Fallklassen aufgrund zusätzlicher 20 Methoden, die Sie durch Zerlegen der Klassendatei sehen können.

Bitte beziehen Sie sich auf diesen Link, wenn Sie alle Methoden innerhalb einer Fallklasse überprüfen möchten .


1
  • Fallklassen definieren ein Compagnon-Objekt mit Apply- und Unapply-Methoden
  • Fallklassen erweitern Serializable
  • Fallklassen definieren gleich hashCode und Kopiermethoden
  • Alle Attribute des Konstruktors sind val (syntaktischer Zucker)

1

Einige der wichtigsten Funktionen von case classessind unten aufgeführt

  1. Fallklassen sind unveränderlich.
  2. Sie können Fallklassen ohne instanziieren new Schlüsselwort .
  3. Fallklassen können nach Wert verglichen werden

Beispiel für einen Scala-Code auf einer Scala-Geige aus den Scala-Dokumenten.

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

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.