Czy istnieją jakieś wytyczne dotyczące najlepszych praktyk dotyczące tego, kiedy używać klas przypadków (lub obiektów spraw) zamiast rozszerzania Wyliczania w Scali?
Wydaje się, że oferują te same korzyści.
enum
na Dotty (z połowy 2020 r.).
Czy istnieją jakieś wytyczne dotyczące najlepszych praktyk dotyczące tego, kiedy używać klas przypadków (lub obiektów spraw) zamiast rozszerzania Wyliczania w Scali?
Wydaje się, że oferują te same korzyści.
enum
na Dotty (z połowy 2020 r.).
Odpowiedzi:
Jedną dużą różnicą jest to, że Enumeration
są obsługiwane przez tworzenie ich z jakiegoś name
Stringa. Na przykład:
object Currency extends Enumeration {
val GBP = Value("GBP")
val EUR = Value("EUR") //etc.
}
Następnie możesz zrobić:
val ccy = Currency.withName("EUR")
Jest to przydatne, gdy chcesz zachować wyliczenia (na przykład w bazie danych) lub utworzyć je z danych znajdujących się w plikach. Jednak ogólnie uważam, że wyliczenia są nieco nieporadne w Scali i mam wrażenie niezręcznego dodatku, więc teraz mam tendencję do używania case object
s. A case object
jest bardziej elastyczny niż wyliczenie:
sealed trait Currency { def name: String }
case object EUR extends Currency { val name = "EUR" } //etc.
case class UnknownCurrency(name: String) extends Currency
Więc teraz mam tę zaletę ...
trade.ccy match {
case EUR =>
case UnknownCurrency(code) =>
}
As @ chaotic3quilibrium wskazał (z pewnymi poprawkami ułatwiającymi czytanie):
Jeśli chodzi o wzorzec „UnknownCurrency (code)”, istnieją inne sposoby radzenia sobie ze znalezieniem łańcucha kodu waluty niż „zerwanie” zamkniętego zestawu tego
Currency
typu.UnknownCurrency
być w typieCurrency
może teraz zakraść się do innych części API.Wskazane jest, aby wypchnąć tę skrzynkę na zewnątrz
Enumeration
i zmusić klienta do radzenia sobie zOption[Currency]
typem, który wyraźnie wskazywałby, że naprawdę istnieje pasujący problem i „zachęcał” użytkownika interfejsu API do samodzielnego rozwiązania problemu.
Aby odpowiedzieć na inne odpowiedzi tutaj, główne wady case object
s ponad Enumeration
s to:
Nie można iterować we wszystkich przypadkach „wyliczenia” . Z pewnością tak jest, ale w praktyce stwierdziłem, że jest to niezwykle rzadkie.
Nie można łatwo utworzyć instancji z utrwalonej wartości . Jest to również prawdą, ale z wyjątkiem dużych wyliczeń (na przykład wszystkich walut), nie stanowi to dużego obciążenia.
trade.ccy
w przykładzie zapieczętowanej cechy.
case
object
generują większego (~ 4x) śladu kodu niż Enumeration
? Przydatne rozróżnienie, szczególnie w przypadku scala.js
projektów wymagających małej powierzchni.
AKTUALIZACJA: Utworzono nowe rozwiązanie oparte na makrach , które jest znacznie lepsze niż rozwiązanie, które przedstawiam poniżej. Zdecydowanie polecam korzystanie z tego nowego rozwiązania opartego na makrach . I wydaje się, że plany Dotty sprawią, że ten styl rozwiązania enum będzie częścią języka. Whoohoo!
Podsumowanie:
Istnieją trzy podstawowe wzorce próby odtworzenia Javy Enum
w ramach projektu Scala. Dwa z trzech wzorów; bezpośrednio przy użyciu Java Enum
i scala.Enumeration
nie są w stanie umożliwić wyczerpującego dopasowania wzorca Scali. I trzeci; „Sealed trait + case object”, ma… ale ma komplikacje związane z inicjalizacją klasy / obiektu JVM, co powoduje niespójne generowanie indeksu porządkowego.
Stworzyłem rozwiązanie z dwiema klasami; Wyliczenie i wyliczenie Udekorowane , znajdujące się w tej Gist . Nie opublikowałem kodu w tym wątku, ponieważ plik wyliczenia był dość duży (+400 wierszy - zawiera wiele komentarzy wyjaśniających kontekst implementacji).
Szczegóły:
Pytanie, które zadajesz, jest dość ogólne; „... kiedy używać case
klasobjects
zamiast rozszerzania [scala.]Enumeration
”. I okazuje się, że istnieje WIELE możliwych odpowiedzi, każda odpowiedź zależy od subtelności określonych wymagań projektu. Odpowiedź można zredukować do trzech podstawowych wzorów.
Na początek upewnijmy się, że pracujemy od tego samego podstawowego pojęcia, czym jest wyliczenie. Zdefiniujmy wyliczenie głównie w kategoriach Enum
dostarczonych od Java 5 (1.5) :
Enum
, byłoby miło móc jawnie wykorzystać sprawdzanie wyczerpania dopasowania wzorca Scali w celu wyliczenia Następnie spójrzmy na sprowadzone wersje trzech najczęściej publikowanych wzorców rozwiązań:
A) Właściwie bezpośrednio przy użyciu wzorca JavaEnum
(w mieszanym projekcie Scala / Java):
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;
}
}
Następujące elementy z definicji wyliczenia nie są dostępne:
W przypadku moich bieżących projektów nie czerpię korzyści z ryzyka związanego ze ścieżką projektów mieszanych Scala / Java. I nawet gdybym mógł wybrać projekt mieszany, punkt 7 jest krytyczny, ponieważ pozwala mi wychwycić problemy z czasem kompilacji, jeśli / kiedy dodam / usunę elementy wyliczeniowe lub piszę nowy kod, aby poradzić sobie z istniejącymi elementami wyliczającymi.
B) Używając wzorca „ sealed trait
+case objects
”:
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}
}
Następujące elementy z definicji wyliczenia nie są dostępne:
Można argumentować, że naprawdę spełnia pozycje 5 i 6. definicji wyliczenia. W przypadku 5 trudno jest stwierdzić, że jest wydajna. W przypadku wersji 6 rozszerzenie nie jest tak naprawdę łatwe do przechowywania dodatkowych powiązanych danych dotyczących singletonowości.
C) Używając scala.Enumeration
wzorca (zainspirowanego odpowiedzią StackOverflow ):
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]
}
Następujące elementy z definicji wyliczenia nie są dostępne (okazuje się, że są identyczne z listą do bezpośredniego używania Java Enum):
Ponownie w moich bieżących projektach punkt 7 ma kluczowe znaczenie dla umożliwienia mi wychwycenia problemów z czasem kompilacji, jeśli / kiedy dodam / usunę elementy wyliczeniowe lub piszę nowy kod, aby poradzić sobie z istniejącymi elementami wyliczającymi.
Biorąc pod uwagę powyższą definicję wyliczenia, żadne z powyższych trzech rozwiązań nie działa, ponieważ nie zapewniają one wszystkiego, co opisano w powyższej definicji wyliczenia:
Każde z tych rozwiązań można ostatecznie przerobić / rozszerzyć / zrefaktoryzować, aby spróbować zaspokoić niektóre z brakujących wymagań każdego z nich. Jednak ani Java, Enum
aniscala.Enumeration
rozwiązania nie mogą być wystarczająco rozbudowane, aby zapewnić pozycję 7. A dla moich własnych projektów jest to jedna z bardziej przekonujących wartości używania zamkniętego typu w Scali. Zdecydowanie wolę kompilować ostrzeżenia / błędy czasowe, aby wskazać, że mam lukę / problem w moim kodzie, zamiast konieczności zbierania go z wyjątku / awarii środowiska wykonawczego.
W tym względzie zacząłem pracować ze case object
ścieżką, aby sprawdzić, czy mogę stworzyć rozwiązanie, które obejmowałoby wszystkie powyższe definicje wyliczeń. Pierwszym wyzwaniem było przepchnięcie rdzenia problemu inicjowania klasy / obiektu JVM (szczegółowo opisanego w tym poście StackOverflow ). I w końcu udało mi się znaleźć rozwiązanie.
Ponieważ moim rozwiązaniem są dwie cechy; Wyliczanie i wyliczanie Udekorowane , a ponieważ Enumeration
cecha ma ponad +400 linii (wiele komentarzy wyjaśniających kontekst), rezygnuję z wklejania jej do tego wątku (co spowodowałoby znaczne rozciągnięcie strony). Aby uzyskać szczegółowe informacje, przejdź bezpośrednio do Gist .
Oto, jak rozwiązanie wygląda tak, używając tego samego pomysłu danych jak powyżej (w pełni skomentowana wersja dostępna tutaj ) i wdrożonego w 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
}
Jest to przykładowe użycie nowej pary cech wyliczenia, które utworzyłem (znajdujących się w tej Gist ) w celu wdrożenia wszystkich możliwości pożądanych i opisanych w definicji wyliczenia.
Jednym z wyrażonych problemów jest to, że nazwy członków wyliczenia muszą zostać powtórzone ( decorationOrderedSet
w powyższym przykładzie). Chociaż zminimalizowałem to do jednego powtórzenia, nie mogłem zobaczyć, jak to zrobić jeszcze mniej z powodu dwóch problemów:
getClass.getDeclaredClasses
ma niezdefiniowaną kolejność (i jest mało prawdopodobne, aby była w tej samej kolejności, co case object
deklaracje w kodzie źródłowym)Biorąc pod uwagę te dwa problemy, musiałem zrezygnować z próby wygenerowania dorozumianego zamówienia i musiałem wyraźnie wymagać od klienta zdefiniowania i zadeklarowania go za pomocą jakiegoś uporządkowanego pojęcia zestawu. Ponieważ kolekcje Scali nie mają implementacji zestawu z wstawionymi wstawkami, najlepsze, co mogłem zrobić, to List
sprawdzić, a następnie sprawdzić, czy był to naprawdę zestaw. Nie tak wolałbym to osiągnąć.
Biorąc pod uwagę, że projekt wymagał drugiego uporządkowania listy / zestawu val
, biorąc pod uwagę ChessPiecesEnhancedDecorated
powyższy przykład, można było dodać, case object PAWN2 extends Member
a następnie zapomnieć o dodaniu Decoration(PAWN2,'P2', 2)
do decorationOrderedSet
. Tak więc sprawdzanie środowiska wykonawczego sprawdza, czy lista nie jest tylko zestawem, ale zawiera WSZYSTKIE obiekty sprawy, które rozszerzają sealed trait Member
. To była specjalna forma refleksji / makro piekła do przepracowania.
Zostaw komentarz i / lub opinię na temat Gist .
org.scalaolio.util.Enumeration
a org.scalaolio.util.EnumerationDecorated
: scalaolio.org
Obiekty Case już zwracają swoją nazwę dla swoich metod toString, więc przekazywanie jej osobno nie jest konieczne. Oto wersja podobna do jho (metody wygody pominięte ze względu na zwięzłość):
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)
}
Obiekty są leniwe; za pomocą vals zamiast tego możemy usunąć listę, ale musimy powtórzyć nazwę:
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") {}
}
Jeśli nie masz nic przeciwko oszukiwaniu, możesz wstępnie załadować wartości wyliczenia za pomocą interfejsu API odbicia lub czegoś takiego jak Google Reflections. Nie leniwe obiekty wielkości liter zapewniają najczystszą składnię:
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
}
Ładne i czyste, ze wszystkimi zaletami klas spraw i wyliczeń Java. Osobiście definiuję wartości wyliczania poza obiektem, aby lepiej dopasować idiomatyczny kod Scala:
object Currency extends Enum[Currency]
sealed trait Currency extends Currency.Value
case object EUR extends Currency
case object GBP extends Currency
Currency.values
, odzyskam tylko te wartości, do których wcześniej uzyskałem dostęp. Czy jest na to jakiś sposób?
Zalety korzystania z klas spraw w porównaniu z wyliczeniami to:
Korzyści z używania Wyliczeń zamiast klas spraw są:
Ogólnie rzecz biorąc, jeśli potrzebujesz tylko listy prostych stałych według nazwy, użyj wyliczeń. W przeciwnym razie, jeśli potrzebujesz czegoś bardziej złożonego lub chcesz dodatkowego bezpieczeństwa kompilatora informującego, czy masz określone wszystkie dopasowania, użyj klas przypadków.
AKTUALIZACJA: Poniższy kod zawiera błąd opisany tutaj . Poniższy program testowy działa, ale jeśli użyjesz DayOfWeek.Mon (na przykład) przed samym DayOfWeek, zakończy się niepowodzeniem, ponieważ DayOfWeek nie został zainicjowany (użycie obiektu wewnętrznego nie powoduje zainicjowania obiektu zewnętrznego). Nadal możesz używać tego kodu, jeśli robisz coś val enums = Seq( DayOfWeek )
w swojej klasie głównej, zmuszając do inicjacji swoich wyliczeń lub możesz użyć modyfikacji chaotycznego 3quilibrium. Czekamy na wyliczenie oparte na makrach!
Jeśli chcesz
wówczas mogą być interesujące następujące rzeczy. Witamy mile widziane.
W tej implementacji istnieją abstrakcyjne klasy podstawowe Enum i EnumVal, które rozszerzamy. Za chwilę zobaczymy te klasy, ale najpierw oto, jak zdefiniujesz wyliczenie:
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()
}
Zauważ, że musisz użyć każdej wartości wyliczenia (wywołaj jej metodę zastosuj), aby wprowadzić ją w życie. [Chciałbym, żeby przedmioty wewnętrzne nie były leniwe, chyba że o to poproszę. Myślę.]
Możemy oczywiście dodawać metody / dane do obiektów DayOfWeek, Val lub indywidualnych obiektów przypadków, jeśli chcemy.
A oto jak użyłbyś takiego wyliczenia:
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"
}
}
Oto, co otrzymujesz po skompilowaniu:
DayOfWeekTest.scala:31: warning: match is not exhaustive!
missing combination Sat
missing combination Sun
def aufDeutsch( day: DayOfWeek.Val ) = day match {
^
one warning found
Możesz zamienić „dopasowanie dnia” na „dopasowanie dnia (dzień: @ odznaczone)”, jeśli nie chcesz takich ostrzeżeń, lub po prostu dołącz na końcu sprawę typu „catch-all”.
Po uruchomieniu powyższego programu otrzymujesz następujące dane wyjściowe:
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
Zauważ, że ponieważ Lista i Mapy są niezmienne, możesz łatwo usuwać elementy, aby tworzyć podzbiory, bez przerywania samego wyliczenia.
Oto sama klasa Enum (i EnumVal w niej):
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 )
}
}
}
A oto jego bardziej zaawansowane zastosowanie, które kontroluje identyfikatory i dodaje dane / metody do abstrakcji Val i samego wyliczenia:
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
] jest granicznym grzechem śmiertelnym w świecie FP” - nie sądzę, że opinia ta jest powszechnie akceptowana.
Mam tutaj fajną prostą bibliotekę lib, która pozwala ci używać zapieczętowanych cech / klas jako wartości wyliczeniowych bez konieczności utrzymywania własnej listy wartości. Opiera się na prostym makrze, które nie jest zależne od buggy knownDirectSubclasses
.
Aktualizacja z marca 2017 r .: jak skomentował Anthony Accioly , scala.Enumeration/enum
PR został zamknięty.
Dotty (kompilator nowej generacji dla Scali) przejmie wiodącą rolę, choć wydanie dotty 1970 i PR Martina Oderskiego 1958 .
Uwaga: istnieje teraz (sierpień 2016, ponad 6 lat później) propozycja usunięcia scala.Enumeration
: PR 5352
Przestarzałe
scala.Enumeration
, dodaj@enum
adnotacjęSkładnia
@enum
class Toggle {
ON
OFF
}
jest możliwym przykładem implementacji, intencją jest także wsparcie ADT, które są zgodne z pewnymi ograniczeniami (brak zagnieżdżania, rekurencji lub zmieniania parametrów konstruktora), np .:
@enum
sealed trait Toggle
case object ON extends Toggle
case object OFF extends Toggle
Przestarzała, która jest katastrofą nieoznaczoną
scala.Enumeration
.Zalety @enum w porównaniu do scala.
- Właściwie działa
- Interop Java
- Brak problemów z usuwaniem
- Nie ma mylącego mini-DSL do nauki podczas definiowania wyliczeń
Wady: brak.
Rozwiązuje to problem braku możliwości posiadania jednej bazy kodu obsługującej Scala-JVM
Scala.js
i Scala-Native (kod źródłowy Java nie jest obsługiwanyScala.js/Scala-Native
, kod źródłowy Scala nie jest w stanie zdefiniować wyliczeń, które są akceptowane przez istniejące interfejsy API w Scala-JVM).
Kolejna wada klas spraw w porównaniu z wyliczeniami, gdy trzeba będzie iterować lub filtrować we wszystkich instancjach. Jest to wbudowana funkcja wyliczania (i także wyliczenia Java), podczas gdy klasy przypadków nie obsługują automatycznie takiej możliwości.
Innymi słowy: „nie ma łatwego sposobu na uzyskanie listy całkowitego zestawu wyliczonych wartości z klasami przypadków”.
Jeśli poważnie myślisz o utrzymaniu współdziałania z innymi językami JVM (np. Java), najlepszą opcją jest pisanie wyliczeń Java. Działają one transparentnie zarówno z kodu Scala, jak i kodu Java, co jest więcej niż można powiedzieć o scala.Enumeration
obiektach lub przypadkach. Nie będziemy mieć nowej biblioteki wyliczeń dla każdego nowego projektu hobby na GitHub, jeśli można tego uniknąć!
Widziałem różne wersje tworzenia klas przypadków naśladujących wyliczenie. Oto moja wersja:
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)
}
}
Co pozwala budować klasy spraw, które wyglądają następująco:
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)
}
Może ktoś mógłby wymyślić lepszy trik niż po prostu dodać każdą klasę spraw do listy, tak jak ja. To było wszystko, co mogłem wtedy wymyślić.
Kilka razy zastanawiałem się nad tymi dwiema opcjami, kiedy kilka razy ich potrzebowałem. Do niedawna preferowałem opcję zapieczętowanej cechy obiektu / sprawy.
1) Deklaracja wyliczenia Scala
object OutboundMarketMakerEntryPointType extends Enumeration {
type OutboundMarketMakerEntryPointType = Value
val Alpha, Beta = Value
}
2) Zapieczętowane cechy + przedmioty skrzynek
sealed trait OutboundMarketMakerEntryPointType
case object AlphaEntryPoint extends OutboundMarketMakerEntryPointType
case object BetaEntryPoint extends OutboundMarketMakerEntryPointType
Chociaż żadne z nich tak naprawdę nie spełnia wszystkich warunków wyliczenia java, poniżej przedstawiono zalety i wady:
Wyliczenie Scala
Plusy: -Funkcje do tworzenia instancji z opcją lub bezpośredniego zakładania dokładności (łatwiejsze przy ładowaniu z trwałego sklepu) -Interacja wszystkich możliwych wartości jest obsługiwana
Wady: -Ostrzeżenie kompilacji dla niewyczerpującego wyszukiwania nie jest obsługiwane (sprawia, że dopasowanie wzorca jest mniej idealne)
Obiekty przedmiotów / Zapieczętowane cechy
Plusy: -Używając cech zapieczętowanych, możemy wstępnie utworzyć instancję niektórych wartości, podczas gdy inne można wstrzykiwać podczas tworzenia - pełna obsługa dopasowania wzorców (zdefiniowane metody zastosowania / zastosowania)
Minusy: -Instantingowanie z trwałego sklepu - często musisz tutaj zastosować dopasowanie wzorca lub zdefiniować własną listę wszystkich możliwych „wartości wyliczeniowych”
To, co ostatecznie skłoniło mnie do zmiany zdania, to coś w rodzaju następującego fragmentu kodu:
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
}
Te .get
rozmowy były ohydne - stosując wyliczenie zamiast mogę po prostu wywołać metodę withName na wyliczenie w sposób następujący:
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)
}
}
Więc myślę, że preferuję używanie Wyliczeń, gdy dostęp do wartości ma być uzyskiwany z repozytorium, a w przeciwnym wypadku obiekty / cechy zapieczętowane.
wolę case objects
(to kwestia osobistych preferencji). Aby poradzić sobie z problemami związanymi z tym podejściem (analizowanie łańcucha i iteracja po wszystkich elementach), dodałem kilka wierszy, które nie są idealne, ale są skuteczne.
Wklejam tutaj kod, oczekując, że może być przydatny, a także, że inni mogą go ulepszyć.
/**
* 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 }
}
Dla tych, którzy wciąż szukają sposobu, aby uzyskać odpowiedź GatesDa na działanie : Możesz po prostu odwołać się do obiektu sprawy po zadeklarowaniu go do utworzenia instancji:
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
}
Myślę, że największą zaletą case classes
nad enumerations
tym jest to, że można użyć wzorca klasy typu aka polimorfizm ad-hoc . Nie musisz dopasowywać wyliczeń, takich jak:
someEnum match {
ENUMA => makeThis()
ENUMB => makeThat()
}
zamiast tego będziesz mieć coś takiego:
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() = ...
}