Gibt es Best-Practice-Richtlinien für die Verwendung von Fallklassen (oder Fallobjekten) im Vergleich zur Erweiterung der Aufzählung in Scala?
Sie scheinen einige der gleichen Vorteile zu bieten.
enum
(für Mitte 2020).
Gibt es Best-Practice-Richtlinien für die Verwendung von Fallklassen (oder Fallobjekten) im Vergleich zur Erweiterung der Aufzählung in Scala?
Sie scheinen einige der gleichen Vorteile zu bieten.
enum
(für Mitte 2020).
Antworten:
Ein großer Unterschied besteht darin, dass Enumeration
die Instanziierung von einem name
String unterstützt wird. Beispielsweise:
object Currency extends Enumeration {
val GBP = Value("GBP")
val EUR = Value("EUR") //etc.
}
Dann können Sie tun:
val ccy = Currency.withName("EUR")
Dies ist nützlich, wenn Sie Aufzählungen (z. B. in einer Datenbank) beibehalten oder aus Daten erstellen möchten, die sich in Dateien befinden. Im Allgemeinen finde ich jedoch, dass Aufzählungen in Scala etwas ungeschickt sind und das Gefühl eines umständlichen Add-Ons haben, weshalb ich jetzt eher case object
s verwende. A case object
ist flexibler als eine Aufzählung:
sealed trait Currency { def name: String }
case object EUR extends Currency { val name = "EUR" } //etc.
case class UnknownCurrency(name: String) extends Currency
Jetzt habe ich den Vorteil ...
trade.ccy match {
case EUR =>
case UnknownCurrency(code) =>
}
Wie @ chaotic3quilibrium hervorhob (mit einigen Korrekturen, um das Lesen zu erleichtern):
In Bezug auf das Muster "UnknownCurrency (Code)" gibt es andere Möglichkeiten, um keine Währungscode-Zeichenfolge zu finden, als die geschlossene Menge des
Currency
Typs zu "brechen" .UnknownCurrency
Wenn Sie vom Typ sind,Currency
können Sie sich jetzt in andere Teile einer API einschleichen.Es ist ratsam, diesen Fall nach außen zu verschieben
Enumeration
und den Kunden dazu zu bringen, sich mit einemOption[Currency]
Typ zu befassen , der eindeutig anzeigt, dass tatsächlich ein Übereinstimmungsproblem vorliegt, und den Benutzer der API zu "ermutigen", es selbst zu klären.
Um die anderen Antworten hier weiterzuverfolgen, sind die Hauptnachteile von case object
s über Enumeration
s:
Kann nicht über alle Instanzen der "Aufzählung" iterieren . Dies ist sicherlich der Fall, aber ich habe es in der Praxis als äußerst selten empfunden, dass dies erforderlich ist.
Kann nicht einfach aus dem dauerhaften Wert instanziieren . Dies gilt auch, aber außer bei großen Aufzählungen (z. B. allen Währungen) bedeutet dies keinen großen Aufwand.
trade.ccy
im Beispiel des versiegelten Merkmals übereinstimmt, nicht .
case
object
erzeugen Sie keinen größeren (~ 4x) Code-Footprint als Enumeration
? Nützliche Unterscheidung insbesondere für scala.js
Projekte mit geringem Platzbedarf.
UPDATE: Es wurde eine neue makrobasierte Lösung erstellt, die der unten beschriebenen Lösung weit überlegen ist. Ich empfehle dringend, diese neue makrobasierte Lösung zu verwenden . Und es scheint, dass Pläne für Dotty diesen Stil der Enum-Lösung zu einem Teil der Sprache machen werden. Whoohoo!
Zusammenfassung:
Es gibt drei grundlegende Muster für den Versuch, Java Enum
in einem Scala-Projekt zu reproduzieren . Zwei der drei Muster; direkt mit Java Enum
und scala.Enumeration
können den umfassenden Mustervergleich von Scala nicht aktivieren. Und der dritte; "Sealed Trait + Case Object", tut dies ... hat jedoch Komplikationen bei der Initialisierung von JVM-Klassen / Objekten, die zu einer inkonsistenten Generierung des Ordnungsindex führen.
Ich habe eine Lösung mit zwei Klassen erstellt. Enumeration und EnumerationDecorated befinden sich in dieser Übersicht . Ich habe den Code nicht in diesen Thread gepostet, da die Datei für die Aufzählung ziemlich groß war (+400 Zeilen - enthält viele Kommentare, die den Implementierungskontext erklären).
Details:
Die Frage, die Sie stellen, ist ziemlich allgemein; "... wann man case
Klassen benutzt oderobjects
erweitert [scala.]Enumeration
". Und es stellt sich heraus, dass es VIELE mögliche Antworten gibt, wobei jede Antwort von den Feinheiten der spezifischen Projektanforderungen abhängt, die Sie haben. Die Antwort kann auf drei Grundmuster reduziert werden.
Stellen wir zunächst sicher, dass wir von der gleichen Grundidee einer Aufzählung ausgehen. Definieren wir eine Aufzählung hauptsächlich anhand der Enum
ab Java 5 (1.5) bereitgestellten :
Enum
, wäre es schön, Scalas Musterübereinstimmungsprüfung für eine Aufzählung explizit nutzen zu können Schauen wir uns als nächstes die zusammengefassten Versionen der drei am häufigsten veröffentlichten Lösungsmuster an:
A) Tatsächlich direkt mit Java-Enum
Muster (in einem gemischten Scala / Java-Projekt):
public enum ChessPiece {
KING('K', 0)
, QUEEN('Q', 9)
, BISHOP('B', 3)
, KNIGHT('N', 3)
, ROOK('R', 5)
, PAWN('P', 1)
;
private char character;
private int pointValue;
private ChessPiece(char character, int pointValue) {
this.character = character;
this.pointValue = pointValue;
}
public int getCharacter() {
return character;
}
public int getPointValue() {
return pointValue;
}
}
Die folgenden Elemente aus der Aufzählungsdefinition sind nicht verfügbar:
Bei meinen aktuellen Projekten habe ich nicht den Vorteil, die Risiken rund um den gemischten Scala / Java-Projektpfad einzugehen. Und selbst wenn ich mich für ein gemischtes Projekt entscheiden könnte, ist Punkt 7 entscheidend, damit ich Probleme mit der Kompilierungszeit erkennen kann, wenn ich Aufzählungselemente hinzufüge / entferne oder neuen Code schreibe, um mit vorhandenen Aufzählungsmitgliedern umzugehen.
B) Verwenden des " sealed trait
+case objects
" - Musters:
sealed trait ChessPiece {def character: Char; def pointValue: Int}
object ChessPiece {
case object KING extends ChessPiece {val character = 'K'; val pointValue = 0}
case object QUEEN extends ChessPiece {val character = 'Q'; val pointValue = 9}
case object BISHOP extends ChessPiece {val character = 'B'; val pointValue = 3}
case object KNIGHT extends ChessPiece {val character = 'N'; val pointValue = 3}
case object ROOK extends ChessPiece {val character = 'R'; val pointValue = 5}
case object PAWN extends ChessPiece {val character = 'P'; val pointValue = 1}
}
Die folgenden Elemente aus der Aufzählungsdefinition sind nicht verfügbar:
Es ist fraglich, ob es die Aufzählungsdefinitionspunkte 5 und 6 wirklich erfüllt. Für 5 ist es eine Strecke zu behaupten, dass es effizient ist. Für 6 ist es nicht einfach, zusätzliche zugehörige Singleton-Daten zu speichern.
C) Verwenden des scala.Enumeration
Musters (inspiriert von dieser StackOverflow-Antwort ):
object ChessPiece extends Enumeration {
val KING = ChessPieceVal('K', 0)
val QUEEN = ChessPieceVal('Q', 9)
val BISHOP = ChessPieceVal('B', 3)
val KNIGHT = ChessPieceVal('N', 3)
val ROOK = ChessPieceVal('R', 5)
val PAWN = ChessPieceVal('P', 1)
protected case class ChessPieceVal(character: Char, pointValue: Int) extends super.Val()
implicit def convert(value: Value) = value.asInstanceOf[ChessPieceVal]
}
Die folgenden Elemente aus der Aufzählungsdefinition sind nicht verfügbar (identisch mit der Liste für die direkte Verwendung der Java-Enumeration):
Auch für meine aktuellen Projekte ist Punkt 7 von entscheidender Bedeutung, damit ich Probleme mit der Kompilierungszeit erkennen kann, wenn ich Aufzählungselemente hinzufüge / entferne oder neuen Code für vorhandene Aufzählungsmitglieder schreibe.
Angesichts der obigen Definition einer Aufzählung funktioniert keine der drei oben genannten Lösungen, da sie nicht alles bieten, was in der obigen Aufzählungsdefinition beschrieben ist:
Jede dieser Lösungen kann eventuell überarbeitet / erweitert / überarbeitet werden, um zu versuchen, einige der fehlenden Anforderungen jedes einzelnen zu decken. Weder Java Enum
noch die scala.Enumeration
Lösungen können jedoch ausreichend erweitert werden, um Punkt 7 bereitzustellen. Für meine eigenen Projekte ist dies einer der überzeugendsten Werte bei der Verwendung eines geschlossenen Typs in Scala. Ich bevorzuge Warnungen / Fehler zur Kompilierungszeit, um anzuzeigen, dass ich eine Lücke / ein Problem in meinem Code habe, anstatt es aus einer Ausnahme / einem Fehler zur Produktionslaufzeit herauslesen zu müssen.
In diesem Zusammenhang habe ich mich daran gemacht, mit dem case object
Pfad zu arbeiten , um zu sehen, ob ich eine Lösung finden kann, die alle oben genannten Aufzählungsdefinitionen abdeckt. Die erste Herausforderung bestand darin, den Kern des Problems der JVM-Klassen- / Objektinitialisierung durchzuarbeiten (das in diesem StackOverflow-Beitrag ausführlich behandelt wird ). Und ich konnte endlich eine Lösung finden.
Als meine Lösung sind zwei Eigenschaften; Enumeration und EnumerationDecorated , und da das Enumeration
Merkmal mehr als 400 Zeilen lang ist (viele Kommentare, die den Kontext erklären), verzichte ich darauf, es in diesen Thread einzufügen (was dazu führen würde, dass es die Seite erheblich ausdehnt). Für Details springen Sie bitte direkt zum Kern .
So sieht die Lösung aus, wenn dieselbe Datenidee wie oben (vollständig kommentierte Version hier verfügbar ) verwendet und in implementiert wird EnumerationDecorated
.
import scala.reflect.runtime.universe.{TypeTag,typeTag}
import org.public_domain.scala.utils.EnumerationDecorated
object ChessPiecesEnhancedDecorated extends EnumerationDecorated {
case object KING extends Member
case object QUEEN extends Member
case object BISHOP extends Member
case object KNIGHT extends Member
case object ROOK extends Member
case object PAWN extends Member
val decorationOrderedSet: List[Decoration] =
List(
Decoration(KING, 'K', 0)
, Decoration(QUEEN, 'Q', 9)
, Decoration(BISHOP, 'B', 3)
, Decoration(KNIGHT, 'N', 3)
, Decoration(ROOK, 'R', 5)
, Decoration(PAWN, 'P', 1)
)
final case class Decoration private[ChessPiecesEnhancedDecorated] (member: Member, char: Char, pointValue: Int) extends DecorationBase {
val description: String = member.name.toLowerCase.capitalize
}
override def typeTagMember: TypeTag[_] = typeTag[Member]
sealed trait Member extends MemberDecorated
}
Dies ist ein Beispiel für die Verwendung eines neuen Paares von Aufzählungsmerkmalen, die ich erstellt habe (in dieser Übersicht ), um alle in der Aufzählungsdefinition gewünschten und beschriebenen Funktionen zu implementieren.
Ein Anliegen ist, dass die Namen der Aufzählungsmitglieder wiederholt werden müssen ( decorationOrderedSet
im obigen Beispiel). Obwohl ich es auf eine einzige Wiederholung reduziert habe, konnte ich aufgrund zweier Probleme nicht sehen, wie ich es noch weniger machen kann:
getClass.getDeclaredClasses
hat eine undefinierte Reihenfolge (und es ist ziemlich unwahrscheinlich, dass er in derselben Reihenfolge wie die case object
Deklarationen im Quellcode vorliegt).Angesichts dieser beiden Probleme musste ich den Versuch aufgeben, eine implizite Bestellung zu generieren, und ich musste explizit verlangen, dass der Kunde sie mit einer Art geordnetem Satz definiert und deklariert. Da die Scala-Sammlungen keine Implementierung für geordnete Sätze haben, konnte ich am besten eine List
Laufzeitprüfung verwenden und dann überprüfen, ob es sich wirklich um einen Satz handelt. Es ist nicht so, wie ich es vorgezogen hätte, dies erreicht zu haben.
Und angesichts der Konstruktion dieser zweiten Liste / set Bestellung erforderlich val
, angesichts der ChessPiecesEnhancedDecorated
obigen Beispiel war es möglich , hinzufügen case object PAWN2 extends Member
und dann vergessen , hinzuzufügen , Decoration(PAWN2,'P2', 2)
zu decorationOrderedSet
. Daher wird zur Laufzeit überprüft, ob die Liste nicht nur eine Menge ist, sondern ALLE Fallobjekte enthält, die die Liste erweitern sealed trait Member
. Das war eine besondere Form der Reflexion / Makro-Hölle.
Bitte hinterlassen Sie Kommentare und / oder Feedback zum Kern .
org.scalaolio.util.Enumeration
und org.scalaolio.util.EnumerationDecorated
: scalaolio.org
Fallobjekte geben bereits ihren Namen für ihre toString-Methoden zurück, sodass eine separate Übergabe nicht erforderlich ist. Hier ist eine Version ähnlich der von jho (der Kürze halber wurden die Convenience-Methoden weggelassen):
trait Enum[A] {
trait Value { self: A => }
val values: List[A]
}
sealed trait Currency extends Currency.Value
object Currency extends Enum[Currency] {
case object EUR extends Currency
case object GBP extends Currency
val values = List(EUR, GBP)
}
Objekte sind faul; Wenn wir stattdessen vals verwenden, können wir die Liste löschen, müssen aber den Namen wiederholen:
trait Enum[A <: {def name: String}] {
trait Value { self: A =>
_values :+= this
}
private var _values = List.empty[A]
def values = _values
}
sealed abstract class Currency(val name: String) extends Currency.Value
object Currency extends Enum[Currency] {
val EUR = new Currency("EUR") {}
val GBP = new Currency("GBP") {}
}
Wenn Ihnen Betrug nichts ausmacht, können Sie Ihre Aufzählungswerte mithilfe der Reflection-API oder ähnlichem wie Google Reflections vorladen. Nicht faule Fallobjekte bieten Ihnen die sauberste Syntax:
trait Enum[A] {
trait Value { self: A =>
_values :+= this
}
private var _values = List.empty[A]
def values = _values
}
sealed trait Currency extends Currency.Value
object Currency extends Enum[Currency] {
case object EUR extends Currency
case object GBP extends Currency
}
Schön und sauber, mit allen Vorteilen von Fallklassen und Java-Aufzählungen. Persönlich definiere ich die Aufzählungswerte außerhalb des Objekts, um besser mit dem idiomatischen Scala-Code übereinzustimmen:
object Currency extends Enum[Currency]
sealed trait Currency extends Currency.Value
case object EUR extends Currency
case object GBP extends Currency
Currency.values
, erhalte ich nur Werte zurück, auf die ich zuvor zugegriffen habe. Gibt es einen Weg daran vorbei?
Die Verwendung von Fallklassen gegenüber Aufzählungen bietet folgende Vorteile:
Die Verwendung von Aufzählungen anstelle von Fallklassen bietet folgende Vorteile:
Wenn Sie also nur eine Liste einfacher Konstanten nach Namen benötigen, verwenden Sie im Allgemeinen Aufzählungen. Wenn Sie andernfalls etwas Komplexeres benötigen oder die zusätzliche Sicherheit des Compilers Ihnen mitteilen möchte, ob alle Übereinstimmungen angegeben sind, verwenden Sie Fallklassen.
UPDATE: Der folgende Code hat einen Fehler, der hier beschrieben wird . Das folgende Testprogramm funktioniert, aber wenn Sie DayOfWeek.Mon (zum Beispiel) vor DayOfWeek selbst verwenden, schlägt dies fehl, da DayOfWeek nicht initialisiert wurde (die Verwendung eines inneren Objekts bewirkt nicht, dass ein äußeres Objekt initialisiert wird). Sie können diesen Code weiterhin verwenden, wenn Sie etwas wie val enums = Seq( DayOfWeek )
in Ihrer Hauptklasse tun und die Initialisierung Ihrer Aufzählungen erzwingen, oder Sie können die Modifikationen von chaotic3quilibrium verwenden. Ich freue mich auf eine makrobasierte Aufzählung!
Falls Sie es wollen
dann kann das Folgende von Interesse sein. Feedback willkommen.
In dieser Implementierung gibt es abstrakte Enum- und EnumVal-Basisklassen, die Sie erweitern. Wir werden diese Klassen in einer Minute sehen, aber zuerst würden Sie eine Aufzählung folgendermaßen definieren:
object DayOfWeek extends Enum {
sealed abstract class Val extends EnumVal
case object Mon extends Val; Mon()
case object Tue extends Val; Tue()
case object Wed extends Val; Wed()
case object Thu extends Val; Thu()
case object Fri extends Val; Fri()
case object Sat extends Val; Sat()
case object Sun extends Val; Sun()
}
Beachten Sie, dass Sie jeden Enum-Wert verwenden müssen (rufen Sie die Apply-Methode auf), um ihn zum Leben zu erwecken. [Ich wünschte, innere Objekte wären nicht faul, es sei denn, ich fordere sie ausdrücklich dazu auf. Meiner Ansicht nach.]
Wir könnten natürlich Methoden / Daten zu DayOfWeek, Val oder den einzelnen Fallobjekten hinzufügen, wenn wir dies wünschen.
Und so würden Sie eine solche Aufzählung verwenden:
object DayOfWeekTest extends App {
// To get a map from Int id to enum:
println( DayOfWeek.valuesById )
// To get a map from String name to enum:
println( DayOfWeek.valuesByName )
// To iterate through a list of the enum values in definition order,
// which can be made different from ID order, and get their IDs and names:
DayOfWeek.values foreach { v => println( v.id + " = " + v ) }
// To sort by ID or name:
println( DayOfWeek.values.sorted mkString ", " )
println( DayOfWeek.values.sortBy(_.toString) mkString ", " )
// To look up enum values by name:
println( DayOfWeek("Tue") ) // Some[DayOfWeek.Val]
println( DayOfWeek("Xyz") ) // None
// To look up enum values by id:
println( DayOfWeek(3) ) // Some[DayOfWeek.Val]
println( DayOfWeek(9) ) // None
import DayOfWeek._
// To compare enums as ordinals:
println( Tue < Fri )
// Warnings about non-exhaustive pattern matches:
def aufDeutsch( day: DayOfWeek.Val ) = day match {
case Mon => "Montag"
case Tue => "Dienstag"
case Wed => "Mittwoch"
case Thu => "Donnerstag"
case Fri => "Freitag"
// Commenting these out causes compiler warning: "match is not exhaustive!"
// case Sat => "Samstag"
// case Sun => "Sonntag"
}
}
Folgendes erhalten Sie beim Kompilieren:
DayOfWeekTest.scala:31: warning: match is not exhaustive!
missing combination Sat
missing combination Sun
def aufDeutsch( day: DayOfWeek.Val ) = day match {
^
one warning found
Sie können "Tagesübereinstimmung" durch "(Tag: @unchecked) Übereinstimmung" ersetzen, wenn Sie solche Warnungen nicht möchten, oder am Ende einfach einen Sammelfall einfügen.
Wenn Sie das obige Programm ausführen, erhalten Sie folgende Ausgabe:
Map(0 -> Mon, 5 -> Sat, 1 -> Tue, 6 -> Sun, 2 -> Wed, 3 -> Thu, 4 -> Fri)
Map(Thu -> Thu, Sat -> Sat, Tue -> Tue, Sun -> Sun, Mon -> Mon, Wed -> Wed, Fri -> Fri)
0 = Mon
1 = Tue
2 = Wed
3 = Thu
4 = Fri
5 = Sat
6 = Sun
Mon, Tue, Wed, Thu, Fri, Sat, Sun
Fri, Mon, Sat, Sun, Thu, Tue, Wed
Some(Tue)
None
Some(Thu)
None
true
Da die Liste und die Karten unveränderlich sind, können Sie Elemente leicht entfernen, um Teilmengen zu erstellen, ohne die Aufzählung selbst zu beschädigen.
Hier ist die Enum-Klasse selbst (und EnumVal darin):
abstract class Enum {
type Val <: EnumVal
protected var nextId: Int = 0
private var values_ = List[Val]()
private var valuesById_ = Map[Int ,Val]()
private var valuesByName_ = Map[String,Val]()
def values = values_
def valuesById = valuesById_
def valuesByName = valuesByName_
def apply( id : Int ) = valuesById .get(id ) // Some|None
def apply( name: String ) = valuesByName.get(name) // Some|None
// Base class for enum values; it registers the value with the Enum.
protected abstract class EnumVal extends Ordered[Val] {
val theVal = this.asInstanceOf[Val] // only extend EnumVal to Val
val id = nextId
def bumpId { nextId += 1 }
def compare( that:Val ) = this.id - that.id
def apply() {
if ( valuesById_.get(id) != None )
throw new Exception( "cannot init " + this + " enum value twice" )
bumpId
values_ ++= List(theVal)
valuesById_ += ( id -> theVal )
valuesByName_ += ( toString -> theVal )
}
}
}
Und hier ist eine fortgeschrittenere Verwendung, die die IDs steuert und Daten / Methoden zur Val-Abstraktion und zur Aufzählung selbst hinzufügt:
object DayOfWeek extends Enum {
sealed abstract class Val( val isWeekday:Boolean = true ) extends EnumVal {
def isWeekend = !isWeekday
val abbrev = toString take 3
}
case object Monday extends Val; Monday()
case object Tuesday extends Val; Tuesday()
case object Wednesday extends Val; Wednesday()
case object Thursday extends Val; Thursday()
case object Friday extends Val; Friday()
nextId = -2
case object Saturday extends Val(false); Saturday()
case object Sunday extends Val(false); Sunday()
val (weekDays,weekendDays) = values partition (_.isWeekday)
}
var
] ist eine grenzwertige Todsünde in der FP-Welt" - Ich glaube nicht, dass die Meinung allgemein akzeptiert wird.
Ich habe hier eine schöne einfache Bibliothek, mit der Sie versiegelte Merkmale / Klassen als Enum-Werte verwenden können, ohne Ihre eigene Werteliste führen zu müssen. Es basiert auf einem einfachen Makro, das nicht vom Buggy abhängig ist knownDirectSubclasses
.
Update März 2017: Wie von Anthony Accioly kommentiert , wurde die scala.Enumeration/enum
PR geschlossen.
Dotty (Compiler der nächsten Generation für Scala) wird die Führung übernehmen, obwohl Dotty 1970 und Martin Oderskys PR 1958 .
Hinweis: Es gibt jetzt (August 2016, 6+ Jahre später) einen Vorschlag zum Entfernen scala.Enumeration
: PR 5352
Veraltet
scala.Enumeration
, hinzufügen@enum
AnmerkungDie Syntax
@enum
class Toggle {
ON
OFF
}
ist ein mögliches Implementierungsbeispiel. Es ist beabsichtigt, auch ADTs zu unterstützen, die bestimmten Einschränkungen entsprechen (keine Verschachtelung, Rekursion oder Variation der Konstruktorparameter), z.
@enum
sealed trait Toggle
case object ON extends Toggle
case object OFF extends Toggle
Lehnt die absolute Katastrophe ab
scala.Enumeration
.Vorteile von @enum gegenüber scala.Enumeration:
- Funktioniert eigentlich
- Java Interop
- Keine Löschprobleme
- Kein verwirrendes Mini-DSL-Lernen beim Definieren von Aufzählungen
Nachteile: Keine.
Dies behebt das Problem, dass nicht eine Codebasis vorhanden sein kann, die Scala-JVM
Scala.js
und Scala-Native unterstützt (Java-Quellcode wird nicht unterstütztScala.js/Scala-Native
, Scala-Quellcode kann keine Aufzählungen definieren, die von vorhandenen APIs in Scala-JVM akzeptiert werden).
Ein weiterer Nachteil von Fallklassen gegenüber Aufzählungen, wenn Sie über alle Instanzen hinweg iterieren oder filtern müssen. Dies ist eine integrierte Funktion der Aufzählung (und auch der Java-Aufzählungen), während Fallklassen diese Funktion nicht automatisch unterstützen.
Mit anderen Worten: "Es gibt keine einfache Möglichkeit, eine Liste der gesamten Aufzählungswerte mit Fallklassen abzurufen."
Wenn Sie es ernst meinen, die Interoperabilität mit anderen JVM-Sprachen (z. B. Java) aufrechtzuerhalten, ist es am besten, Java-Enums zu schreiben. Diese arbeiten transparent sowohl mit Scala- als auch mit Java-Code, was mehr ist, als für scala.Enumeration
oder Fallobjekte gesagt werden kann. Lassen Sie uns nicht für jedes neue Hobbyprojekt auf GitHub eine neue Aufzählungsbibliothek haben, wenn dies vermieden werden kann!
Ich habe verschiedene Versionen gesehen, in denen eine Fallklasse eine Aufzählung nachahmt. Hier ist meine Version:
trait CaseEnumValue {
def name:String
}
trait CaseEnum {
type V <: CaseEnumValue
def values:List[V]
def unapply(name:String):Option[String] = {
if (values.exists(_.name == name)) Some(name) else None
}
def unapply(value:V):String = {
return value.name
}
def apply(name:String):Option[V] = {
values.find(_.name == name)
}
}
Auf diese Weise können Sie Fallklassen erstellen, die wie folgt aussehen:
abstract class Currency(override name:String) extends CaseEnumValue {
}
object Currency extends CaseEnum {
type V = Site
case object EUR extends Currency("EUR")
case object GBP extends Currency("GBP")
var values = List(EUR, GBP)
}
Vielleicht könnte sich jemand einen besseren Trick einfallen lassen, als einfach eine Fallklasse wie ich zur Liste hinzuzufügen. Das war alles, was ich mir damals einfallen lassen konnte.
Ich habe diese beiden Optionen die letzten Male hin und her gegangen, als ich sie gebraucht habe. Bis vor kurzem habe ich die Option für versiegelte Merkmale / Fallobjekte bevorzugt.
1) Erklärung zur Scala-Aufzählung
object OutboundMarketMakerEntryPointType extends Enumeration {
type OutboundMarketMakerEntryPointType = Value
val Alpha, Beta = Value
}
2) Versiegelte Eigenschaften + Fallobjekte
sealed trait OutboundMarketMakerEntryPointType
case object AlphaEntryPoint extends OutboundMarketMakerEntryPointType
case object BetaEntryPoint extends OutboundMarketMakerEntryPointType
Während keines von beiden wirklich alle Anforderungen einer Java-Aufzählung erfüllt, sind im Folgenden die Vor- und Nachteile aufgeführt:
Scala-Aufzählung
Vorteile: -Funktionen zum Instanziieren mit Option oder zur direkten Annahme einer genauen (einfacher beim Laden aus einem persistenten Speicher) -Iteration über alle möglichen Werte wird unterstützt
Nachteile: -Kompilierungswarnung für nicht erschöpfende Suche wird nicht unterstützt (macht Mustervergleich weniger ideal)
Fallobjekte / Versiegelte Merkmale
Vorteile: - Mit versiegelten Merkmalen können wir einige Werte vorab instanziieren, während andere zum Zeitpunkt der Erstellung injiziert werden können. - Volle Unterstützung für den Mustervergleich (definierte Methoden anwenden / nicht anwenden).
Nachteile: - Ausgehend von einem persistenten Speicher - Sie müssen hier häufig den Mustervergleich verwenden oder eine eigene Liste aller möglichen 'Aufzählungswerte' definieren.
Was mich letztendlich dazu brachte, meine Meinung zu ändern, war so etwas wie der folgende Ausschnitt:
object DbInstrumentQueries {
def instrumentExtractor(tableAlias: String = "s")(rs: ResultSet): Instrument = {
val symbol = rs.getString(tableAlias + ".name")
val quoteCurrency = rs.getString(tableAlias + ".quote_currency")
val fixRepresentation = rs.getString(tableAlias + ".fix_representation")
val pointsValue = rs.getInt(tableAlias + ".points_value")
val instrumentType = InstrumentType.fromString(rs.getString(tableAlias +".instrument_type"))
val productType = ProductType.fromString(rs.getString(tableAlias + ".product_type"))
Instrument(symbol, fixRepresentation, quoteCurrency, pointsValue, instrumentType, productType)
}
}
object InstrumentType {
def fromString(instrumentType: String): InstrumentType = Seq(CurrencyPair, Metal, CFD)
.find(_.toString == instrumentType).get
}
object ProductType {
def fromString(productType: String): ProductType = Seq(Commodity, Currency, Index)
.find(_.toString == productType).get
}
Die .get
Aufrufe waren abscheulich - stattdessen kann ich mit der Aufzählung einfach die withName-Methode für die Aufzählung wie folgt aufrufen:
object DbInstrumentQueries {
def instrumentExtractor(tableAlias: String = "s")(rs: ResultSet): Instrument = {
val symbol = rs.getString(tableAlias + ".name")
val quoteCurrency = rs.getString(tableAlias + ".quote_currency")
val fixRepresentation = rs.getString(tableAlias + ".fix_representation")
val pointsValue = rs.getInt(tableAlias + ".points_value")
val instrumentType = InstrumentType.withNameString(rs.getString(tableAlias + ".instrument_type"))
val productType = ProductType.withName(rs.getString(tableAlias + ".product_type"))
Instrument(symbol, fixRepresentation, quoteCurrency, pointsValue, instrumentType, productType)
}
}
Daher denke ich, dass ich es in Zukunft vorziehen werde, Aufzählungen zu verwenden, wenn auf die Werte aus einem Repository zugegriffen werden soll, und ansonsten Fallobjekte / versiegelte Merkmale.
Ich bevorzuge case objects
(es ist eine Frage der persönlichen Präferenz). Um die mit diesem Ansatz verbundenen Probleme zu bewältigen (Zeichenfolge analysieren und über alle Elemente iterieren), habe ich einige Zeilen hinzugefügt, die nicht perfekt, aber effektiv sind.
Ich füge Ihnen den Code hier ein und erwarte, dass er nützlich sein könnte und dass andere ihn verbessern könnten.
/**
* Enum for Genre. It contains the type, objects, elements set and parse method.
*
* This approach supports:
*
* - Pattern matching
* - Parse from name
* - Get all elements
*/
object Genre {
sealed trait Genre
case object MALE extends Genre
case object FEMALE extends Genre
val elements = Set (MALE, FEMALE) // You have to take care this set matches all objects
def apply (code: String) =
if (MALE.toString == code) MALE
else if (FEMALE.toString == code) FEMALE
else throw new IllegalArgumentException
}
/**
* Enum usage (and tests).
*/
object GenreTest extends App {
import Genre._
val m1 = MALE
val m2 = Genre ("MALE")
assert (m1 == m2)
assert (m1.toString == "MALE")
val f1 = FEMALE
val f2 = Genre ("FEMALE")
assert (f1 == f2)
assert (f1.toString == "FEMALE")
try {
Genre (null)
assert (false)
}
catch {
case e: IllegalArgumentException => assert (true)
}
try {
Genre ("male")
assert (false)
}
catch {
case e: IllegalArgumentException => assert (true)
}
Genre.elements.foreach { println }
}
Für diejenigen, die noch suchen, wie GatesDas Antwort funktioniert : Sie können einfach auf das case-Objekt verweisen, nachdem Sie es deklariert haben, um es zu instanziieren:
trait Enum[A] {
trait Value { self: A =>
_values :+= this
}
private var _values = List.empty[A]
def values = _values
}
sealed trait Currency extends Currency.Value
object Currency extends Enum[Currency] {
case object EUR extends Currency;
EUR //THIS IS ONLY CHANGE
case object GBP extends Currency; GBP //Inline looks better
}
Ich denke, der größte Vorteil von case classes
Over enumerations
ist, dass Sie Typklassenmuster, auch Ad-hoc-Polymorphysmus genannt, verwenden können . Sie müssen keine Aufzählungen wie:
someEnum match {
ENUMA => makeThis()
ENUMB => makeThat()
}
Stattdessen haben Sie so etwas wie:
def someCode[SomeCaseClass](implicit val maker: Maker[SomeCaseClass]){
maker.make()
}
implicit val makerA = new Maker[CaseClassA]{
def make() = ...
}
implicit val makerB = new Maker[CaseClassB]{
def make() = ...
}