Existe-t-il des directives de meilleures pratiques sur le moment d'utiliser les classes de cas (ou les objets de cas) et d'étendre l'énumération dans Scala?
Ils semblent offrir certains des mêmes avantages.
Une grande différence est que Enumeration
s est pris en charge pour les instancier à partir de name
String. Par exemple:
object Currency extends Enumeration {
val GBP = Value("GBP")
val EUR = Value("EUR") //etc.
}
Ensuite, vous pouvez faire:
val ccy = Currency.withName("EUR")
Ceci est utile si vous souhaitez conserver des énumérations (par exemple, dans une base de données) ou les créer à partir de données résidant dans des fichiers. Cependant, je trouve en général que les énumérations sont un peu maladroites dans Scala et donnent l'impression d'un ajout compliqué, alors j'ai tendance à utiliser maintenant case object
s. Un case object
est plus flexible qu'un enum:
sealed trait Currency { def name: String }
case object EUR extends Currency { val name = "EUR" } //etc.
case class UnknownCurrency(name: String) extends Currency
Alors maintenant, j'ai l'avantage de ...
trade.ccy match {
case EUR =>
case UnknownCurrency(code) =>
}
Comme @ chaotic3quilibrium a souligné (avec quelques corrections pour faciliter la lecture):
En ce qui concerne le modèle "UnknownCurrency (code)", il existe d'autres moyens de ne pas trouver de chaîne de code de devise que "casser" la nature d'ensemble fermé du type
Currency
.UnknownCurrency
étant de typeCurrency
peut maintenant se faufiler dans d'autres parties d'une API.Il est conseillé de repousser ce cas en dehors de la variable
Enumeration
et d'obliger le client à utiliser un typeOption[Currency]
qui indiquerait clairement qu'il existe réellement un problème de correspondance et d'encourager l'utilisateur de l'API à le résoudre lui-même.
Pour faire suite aux autres réponses ici, les principaux inconvénients de case object
s sur Enumeration
s sont les suivants:
Impossible de parcourir toutes les instances de "l'énumération". C'est certainement le cas, mais j'ai trouvé extrêmement rare dans la pratique que cela soit nécessaire.
Impossible d'instancier facilement à partir de la valeur persistante. Ceci est également vrai, mais, sauf dans le cas d'énumérations énormes (par exemple, toutes les devises), cela ne représente pas une surcharge considérable.
Les objets case renvoient déjà leur nom pour leurs méthodes toString, il est donc inutile de les transmettre séparément. Voici une version similaire à jho (méthodes de commodité omises pour des raisons de brièveté):
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)
}
Les objets sont paresseux; en utilisant vals à la place, nous pouvons supprimer la liste mais répéter le nom:
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") {}
}
Si vous ne craignez pas de tricher, vous pouvez pré-charger vos valeurs d'énumération à l'aide de l'API de réflexion ou de quelque chose comme Google Reflections. Les objets de cas non paresseux vous donnent la syntaxe la plus propre:
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
}
Nice and clean, avec tous les avantages des classes de cas et des énumérations Java. Personnellement, je définis les valeurs d'énumération en dehors de l'objet pour mieux correspondre au code Scala idiomatique:
object Currency extends Enum[Currency]
sealed trait Currency extends Currency.Value
case object EUR extends Currency
case object GBP extends Currency
L'utilisation des classes de cas par rapport aux énumérations présente les avantages suivants:
L'utilisation des énumérations au lieu des classes de cas présente les avantages suivants:
Donc, en général, si vous avez juste besoin d’une liste de constantes simples par nom, utilisez des énumérations. Sinon, si vous avez besoin de quelque chose d'un peu plus complexe ou si vous souhaitez que le compilateur soit plus sûr en vous indiquant si toutes les correspondances sont spécifiées, utilisez les classes de cas.
UPDATE: Le code ci-dessous a un bogue, décrit ici . Le programme de test ci-dessous fonctionne, mais si vous utilisiez DayOfWeek.Mon (par exemple) avant DayOfWeek lui-même, cela échouerait car DayOfWeek n'a pas été initialisé (l'utilisation d'un objet interne ne provoque pas l'initialisation d'un objet externe). Vous pouvez toujours utiliser ce code si vous faites quelque chose comme val enums = Seq( DayOfWeek )
dans votre classe principale, en forçant l'initialisation de vos énumérations, ou si vous pouvez utiliser les modifications de chaotic3quilibrium. Dans l'attente d'un enum basé sur les macros!
Si tu veux
alors ce qui suit peut être d’intérêt. Commentaires bienvenus.
Dans cette implémentation, il existe des classes de base abstraites Enum et EnumVal, que vous étendez. Nous verrons ces classes dans une minute, mais d’abord, voici comment vous définiriez une énumération:
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()
}
Notez que vous devez utiliser chaque valeur enum (appelez sa méthode apply) pour la concrétiser. [Je souhaite que les objets intérieurs ne soient pas paresseux, sauf si je le leur demande spécifiquement. Je pense.]
Nous pourrions bien sûr ajouter des méthodes/données à DayOfWeek, Val ou aux objets de cas individuels si nous le souhaitions.
Et voici comment vous utiliseriez une telle énumération:
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"
}
}
Voici ce que vous obtenez lorsque vous le compilez:
DayOfWeekTest.scala:31: warning: match is not exhaustive!
missing combination Sat
missing combination Sun
def aufDeutsch( day: DayOfWeek.Val ) = day match {
^
one warning found
Vous pouvez remplacer "match de jour" par "match (jour: @unchecked)" lorsque vous ne voulez pas de tels avertissements, ou simplement inclure un casier à la fin.
Lorsque vous exécutez le programme ci-dessus, vous obtenez cette sortie:
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
Notez que, puisque la liste et les cartes sont immuables, vous pouvez facilement supprimer des éléments pour créer des sous-ensembles, sans interrompre l'énumération elle-même.
Voici la classe Enum elle-même (et EnumVal en son sein):
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 )
}
}
}
Et voici une utilisation plus avancée de celui-ci qui contrôle les identifiants et ajoute des données/méthodes à l'abstraction de Val et à l'énumération même:
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)
}
J'ai ici une bibliothèque simple et agréable qui vous permet d'utiliser des traits/classes scellés comme valeurs enum sans avoir à gérer votre propre liste de valeurs. Il repose sur une macro simple qui ne dépend pas du buggy knownDirectSubclasses
.
Mise à jour de mars 2017: commenté par Anthony Accioly , le scala.Enumeration/enum
PR a été fermé.
Dotty (compilateur de prochaine génération pour Scala) prendra la tête, bien que le numéro de 1970 et Le PR 1958 de Martin Odersky .
Remarque: il existe maintenant (août 2016, 6 ans et plus plus tard) une proposition visant à supprimer scala.Enumeration
: PR 5352
Obsolète
scala.Enumeration
, ajoutez l'annotation@enum
La syntaxe
@enum
class Toggle {
ON
OFF
}
est un exemple d'implémentation possible, l'intention est de prendre également en charge les ADT conformes à certaines restrictions (absence d'imbrication, de récursivité ou de modification des paramètres du constructeur), e. g.:
@enum
sealed trait Toggle
case object ON extends Toggle
case object OFF extends Toggle
Observe le désastre non résolu qui est
scala.Enumeration
.Avantages de @enum sur scala.Enumeration:
- Fonctionne réellement
- Java interop
- Aucun problème d'effacement
- Pas de mini-DSL déroutant à apprendre lors de la définition d'énumérations
Inconvénients: aucun.
Cela résout le problème de l’impossibilité d’avoir une base de code qui prend en charge Scala-JVM,
Scala.js
et Scala-Native (le code source Java n'est pas pris en charge surScala.js/Scala-Native
, le code source Scala ne permet pas de définir les énumérations acceptées par les API existantes sur Scala-JVM).
Un autre inconvénient des classes de cas par rapport aux énumérations lorsque vous devez itérer ou filtrer sur toutes les instances. Il s'agit d'une fonctionnalité intégrée d'énumération (et d'énumérations Java) alors que les classes de cas ne prennent pas automatiquement en charge une telle fonctionnalité.
En d'autres termes: "il n'y a pas de moyen simple d'obtenir une liste de l'ensemble des valeurs énumérées avec les classes de cas".
Si vous tenez vraiment au maintien de l'interopérabilité avec d'autres langages JVM (par exemple, Java), la meilleure option consiste à écrire des énumérations Java. Ceux-ci fonctionnent de manière transparente à partir de code Scala et Java, ce qui est plus que ne peut être dit pour les objets scala.Enumeration
ou case. N'ayons pas de nouvelle bibliothèque d'énumérations pour chaque nouveau projet de loisir sur GitHub, si cela peut être évité!
J'ai vu diverses versions d'une classe de cas imiter une énumération. Voici ma 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)
}
}
Ce qui vous permet de construire des classes de cas qui ressemblent à ceci:
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)
}
Peut-être que quelqu'un pourrait proposer un meilleur truc que d'ajouter simplement une classe de cas à la liste comme je l'ai fait. C’était tout ce que je pouvais trouver à l’époque.
Je suis allé et retour sur ces deux options les dernières fois où j'en ai eu besoin. Jusqu'à récemment, ma préférence a été pour l'option de trait/objet de cas scellé.
1) Déclaration de dénombrement Scala
object OutboundMarketMakerEntryPointType extends Enumeration {
type OutboundMarketMakerEntryPointType = Value
val Alpha, Beta = Value
}
2) Traits scellés + objets de cas
sealed trait OutboundMarketMakerEntryPointType
case object AlphaEntryPoint extends OutboundMarketMakerEntryPointType
case object BetaEntryPoint extends OutboundMarketMakerEntryPointType
Bien qu'aucune de celles-ci ne réponde vraiment à tout ce qu'une énumération de Java vous donne, voici les avantages et les inconvénients:
Enumération Scala
Avantages: - Fonctions pour instancier avec option ou supposer directement exactes (plus facile lors du chargement depuis un magasin persistant) - L'itération sur toutes les valeurs possibles est prise en charge
Inconvénients: - L'avertissement de compilation pour une recherche non exhaustive n'est pas pris en charge (rend la correspondance de modèle moins idéale)
Objets de cas/traits scellés
Avantages: - À l'aide de traits scellés, nous pouvons pré-instancier certaines valeurs, tandis que d'autres peuvent être injectées au moment de la création - Prise en charge complète de la correspondance de modèle (méthodes apply/unapply définies)
Inconvénients: - Instanciation depuis un magasin persistant - vous devez souvent utiliser la correspondance de modèle ici ou définir votre propre liste de toutes les «valeurs énumérées» possibles
Ce qui m'a finalement amené à changer d'avis était quelque chose comme l'extrait suivant:
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
}
Les appels .get
étaient hideux - en utilisant l'énumération à la place, je peux simplement appeler la méthode withName sur l'énumération comme suit:
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)
}
}
Je pense donc que ma préférence pour l’avenir est d’utiliser des énumérations lorsque les valeurs doivent être accessibles à partir d’un référentiel et que les objets de cas/traits scellés sont sinon utilisés.
Je préfère case objects
(c'est une question de préférence personnelle). Pour faire face aux problèmes inhérents à cette approche (chaîne d'analyse et itération de tous les éléments), j'ai ajouté quelques lignes qui ne sont pas parfaites, mais qui sont efficaces.
Je vous colle le code ici en espérant qu'il pourrait être utile et que d'autres pourraient l'améliorer.
/**
* 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 }
}
Je pense que le plus grand avantage d’avoir case classes
sur enumerations
est que vous pouvez utiliser le modèle de classe a.k.a ad-hoc polymorphysm. Vous n'avez pas besoin de faire correspondre des enums comme:
someEnum match {
ENUMA => makeThis()
ENUMB => makeThat()
}
à la place, vous aurez quelque chose comme:
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() = ...
}
Pour ceux qui cherchent encore comment obtenir la réponse de GatesDa au travail : Vous pouvez simplement référencer l'objet case après l'avoir déclaré pour l'instancier:
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
}