wake-up-neo.com

Was sind die Nachteile der Deklaration von Scala case classes?

Wenn Sie Code schreiben, der viele schöne, unveränderliche Datenstrukturen verwendet, scheinen die Fallklassen ein Glücksfall zu sein. Mit nur einem Schlüsselwort erhalten Sie kostenlos Folgendes:

  • Standardmäßig ist alles unveränderlich
  • Getter werden automatisch definiert
  • Ordentliche Implementierung von toString ()
  • Kompatibel mit equals () und hashCode ()
  • Begleitobjekt mit unapply () -Methode zum Abgleichen

Aber was sind die Nachteile der Definition einer unveränderlichen Datenstruktur als Fallklasse?

Welche Einschränkungen werden der Klasse oder ihren Kunden auferlegt?

Gibt es Situationen, in denen Sie eine Klasse ohne Groß- und Kleinschreibung bevorzugen sollten?

102
Graham Lea

Ein großer Nachteil: Eine Case-Klasse kann keine Case-Klasse erweitern. Das ist die Einschränkung.

Weitere Vorteile, die Sie verpasst haben, sind der Vollständigkeit halber aufgeführt: Konforme Serialisierung/Deserialisierung, zum Erstellen muss kein "neues" Schlüsselwort verwendet werden.

Ich bevorzuge Klassen ohne Groß-/Kleinschreibung für Objekte mit veränderlichem Status, privatem Status oder keinem Status (z. B. die meisten Singleton-Komponenten). Fallklassen für so ziemlich alles andere.

49
Dave Griffith

Zuerst die guten Teile:

Standardmäßig ist alles unveränderlich

Ja, und kann bei Bedarf sogar mit var überschrieben werden

Getter automatisch definiert

In jeder Klasse möglich, indem den Parametern val vorangestellt wird

Ordentliche toString() Implementierung

Ja, sehr nützlich, aber bei Bedarf mit der Hand für jede Klasse machbar

Kompatibel mit equals() und hashCode()

In Kombination mit dem einfachen Mustervergleich ist dies der Hauptgrund, warum Benutzer Fallklassen verwenden.

Begleitobjekt mit unapply() Methode zum Abgleichen

Auch mit Extraktoren von Hand in jeder Klasse möglich

Diese Liste sollte auch die überaus leistungsfähige Kopiermethode enthalten, eine der besten Möglichkeiten, die es zu Scala 2.8


Dann das Schlechte, es gibt nur eine Handvoll wirklicher Einschränkungen bei Fallklassen:

Sie können apply im Begleitobjekt nicht mit derselben Signatur wie die vom Compiler generierte Methode definieren

In der Praxis ist dies jedoch selten ein Problem. Das Ändern des Verhaltens der generierten Apply-Methode wird die Benutzer garantiert überraschen und sollte dringend unterbunden werden. Die einzige Rechtfertigung dafür ist die Validierung von Eingabeparametern - eine Aufgabe, die am besten im Hauptkonstruktorkörper erledigt wird (der die Validierung auch bei Verwendung von copy verfügbar macht).

Sie können keine Unterklassen erstellen

Es ist wahr, obwohl es immer noch möglich ist, dass eine Case-Klasse selbst ein Nachkomme ist. Ein häufiges Muster besteht darin, eine Klassenhierarchie von Merkmalen unter Verwendung von Fallklassen als Blattknoten des Baums aufzubauen.

Beachten Sie auch den Modifikator sealed. Jede Unterklasse eines Merkmals mit diesem Modifikator muss muss in derselben Datei deklariert werden. Beim Mustervergleich mit Instanzen des Merkmals kann der Compiler Sie warnen, wenn Sie nicht alle möglichen konkreten Unterklassen überprüft haben. In Kombination mit Fallklassen kann dies Ihnen ein sehr hohes Maß an Vertrauen in Ihren Code bieten, wenn er ohne Vorwarnung kompiliert wird.

Als Unterklasse von Product können Case-Klassen nicht mehr als 22 Parameter enthalten

Keine echte Problemumgehung, außer, dass der Klassenmissbrauch mit so vielen Parametern eingestellt wird :)

Ebenfalls...

Eine andere Einschränkung, die manchmal bemerkt wird, ist, dass Scala unterstützt (derzeit) keine faulen Parameter (wie lazy vals, aber als Parameter). Die Problemumgehung besteht darin, einen By-Name-Parameter zu verwenden und ihn im Konstruktor einem verzögerten Wert zuzuweisen. Leider mischen sich die By-Name-Parameter nicht mit dem Pattern-Matching. Dies verhindert, dass die mit Case-Klassen verwendete Technik den vom Compiler generierten Extraktor unterbricht.

Dies ist relevant, wenn Sie hochfunktionale Lazy-Datenstrukturen implementieren möchten, die hoffentlich durch das Hinzufügen von Lazy-Parametern zu einer zukünftigen Version von Scala behoben werden.

98
Kevin Wright

Ich denke hier gilt das TDD-Prinzip: Überdesigne nicht. Wenn Sie etwas als case class Deklarieren, deklarieren Sie viele Funktionen. Das wird die Flexibilität verringern, die Sie haben, um die Klasse in Zukunft zu ändern.

Beispielsweise hat ein case class Eine equals -Methode über den Konstruktorparametern. Das ist Ihnen vielleicht egal, wenn Sie Ihre Klasse zum ersten Mal schreiben, aber letztere entscheiden möglicherweise, dass Gleichheit einige dieser Parameter ignorieren oder etwas anderes tun soll. In der Zwischenzeit kann jedoch Client-Code geschrieben werden, was von der Gleichheit case class Abhängt.

10

Gibt es Situationen, in denen Sie eine Klasse ohne Groß- und Kleinschreibung bevorzugen sollten?

Martin Odersky gibt uns einen guten Ausgangspunkt in seinem Kurs Funktionsprinzipien der Programmierung in Scala (Vorlesung 4.6 - Pattern Matching), den wir verwenden können, wenn wir zwischen Klasse und Fallklasse wählen müssen. Das Kapitel 7 von Scala By Example enthält dasselbe Beispiel.

Angenommen, wir möchten einen Interpreter für arithmetische Ausdrücke schreiben. Um die Dinge zunächst einfach zu halten, beschränken wir uns auf Zahlen und + Operationen. Solche Ausdrücke können als Klassenhierarchie mit einer abstrakten Basisklasse Expr als Wurzel und zwei Unterklassen Number und Sum dargestellt werden. Dann würde ein Ausdruck 1 + (3 + 7) als dargestellt

neue Summe (neue Nummer (1), neue Summe (neue Nummer (3), neue Nummer (7)))

abstract class Expr {
  def eval: Int
}

class Number(n: Int) extends Expr {
  def eval: Int = n
}

class Sum(e1: Expr, e2: Expr) extends Expr {
  def eval: Int = e1.eval + e2.eval
}

Darüber hinaus hat das Hinzufügen einer neuen Prod-Klasse keine Änderungen am vorhandenen Code zur Folge:

class Prod(e1: Expr, e2: Expr) extends Expr {
  def eval: Int = e1.eval * e2.eval
}

Im Gegensatz dazu erfordert das Hinzufügen einer neuen Methode das Ändern aller vorhandenen Klassen.

abstract class Expr { 
  def eval: Int 
  def print
} 

class Number(n: Int) extends Expr { 
  def eval: Int = n 
  def print { Console.print(n) }
}

class Sum(e1: Expr, e2: Expr) extends Expr { 
  def eval: Int = e1.eval + e2.eval
  def print { 
   Console.print("(")
   print(e1)
   Console.print("+")
   print(e2)
   Console.print(")")
  }
}

Dasselbe Problem wurde mit Fallklassen gelöst.

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
  }
}
case class Number(n: Int) extends Expr
case class Sum(e1: Expr, e2: Expr) extends Expr

Das Hinzufügen einer neuen Methode ist eine lokale Änderung.

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
  }
  def print = this match {
    case Number(n) => Console.print(n)
    case Sum(e1,e2) => {
      Console.print("(")
      print(e1)
      Console.print("+")
      print(e2)
      Console.print(")")
    }
  }
}

Das Hinzufügen einer neuen Prod-Klasse erfordert möglicherweise eine Änderung des gesamten Mustervergleichs.

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
    case Prod(e1,e2) => e1.eval * e2.eval
  }
  def print = this match {
    case Number(n) => Console.print(n)
    case Sum(e1,e2) => {
      Console.print("(")
      print(e1)
      Console.print("+")
      print(e2)
      Console.print(")")
    }
    case Prod(e1,e2) => ...
  }
}

Transkript aus der Videovorlesung 4.6 Pattern Matching

Beide Entwürfe sind vollkommen in Ordnung und die Wahl zwischen ihnen ist manchmal eine Frage des Stils, aber dennoch gibt es einige Kriterien, die wichtig sind.

Ein Kriterium könnte sein: Erstellen Sie häufiger neue Unterklassen von Ausdrücken oder erstellen Sie häufiger neue Methoden? Es handelt sich also um ein Kriterium, das untersucht wird die zukünftige Erweiterbarkeit und den möglichen Erweiterungspass Ihres Systems.

Wenn Sie hauptsächlich neue Unterklassen erstellen, hat die objektorientierte Zerlegungslösung die Oberhand. Der Grund ist, dass es sehr einfach und sehr lokal ist, eine neue Unterklasse mit einer Eval-Methode zu erstellen, bei der Sie wie bei der funktionalen Lösung den Code ändern müssen innerhalb der Eval-Methode und fügen Sie einen neuen Fall hinzu.

Wenn Sie dagegen viele neue Methoden erstellen, aber die Klassenhierarchie selbst relativ stabil bleibt, ist der Musterabgleich tatsächlich von Vorteil. Denn auch hier ist jede neue Methode in der Mustervergleichslösung nur eine lokale Änderung , unabhängig davon, ob Sie sie in die Basisklasse oder sogar außerhalb der Klassenhierarchie einfügen. Während eine neue Methode, wie sie in der objektorientierten Zerlegung gezeigt wird, eine neue Inkrementierung erfordern würde, ist jede Unterklasse. Es gäbe also mehr Teile, die du anfassen musst.

Das Problem dieser Erweiterbarkeit in zwei Dimensionen, bei dem Sie möglicherweise einer Hierarchie neue Klassen hinzufügen oder neue Methoden hinzufügen möchten, oder vielleicht beides, wurde als Ausdrucksproblem bezeichnet .

Denken Sie daran: Wir müssen dies als Ausgangspunkt verwenden und nicht als einziges Kriterium.

enter image description here

6
gabrielgiussi

Ich zitiere dies aus Scala cookbook durch Alvin Alexander Kapitel 6: objects.

Dies ist eines der vielen Dinge, die ich in diesem Buch interessant fand.

Um mehrere Konstruktoren für eine Fallklasse bereitzustellen, ist es wichtig zu wissen, was die Fallklassendeklaration tatsächlich tut.

case class Person (var name: String)

Wenn Sie sich den Code ansehen, den der Scala= - Compiler für das Fallklassenbeispiel generiert, werden Sie feststellen, dass er zwei Ausgabedateien erstellt, Person $ .class und Person.class. Wenn Sie Person disassemblieren $ .class mit dem Befehl javap enthält neben vielen anderen eine Methode apply:

$ javap Person$
Compiled from "Person.scala"
public final class Person$ extends scala.runtime.AbstractFunction1 implements scala.ScalaObject,scala.Serializable{
public static final Person$ MODULE$;
public static {};
public final Java.lang.String toString();
public scala.Option unapply(Person);
public Person apply(Java.lang.String); // the apply method (returns a Person) public Java.lang.Object readResolve();
        public Java.lang.Object apply(Java.lang.Object);
    }

Sie können Person.class auch zerlegen, um zu sehen, was es enthält. Für eine einfache Klasse wie diese enthält sie zusätzliche 20 Methoden. Dieses versteckte Aufblähen ist einer der Gründe, warum einige Entwickler keine Fallklassen mögen.

0
arglee