Jak modelować typy wyliczeniowe bezpieczne dla typu?
311
Scala nie ma bezpiecznych typów, enumtakich jak Java. Biorąc pod uwagę zestaw powiązanych stałych, jaki byłby najlepszy sposób w Scali do przedstawienia tych stałych?
Poważnie, nie należy używać aplikacji. NIE zostało to naprawione; wprowadzono nową klasę, App, która nie ma problemów, o których wspomniał Schildmeijer. Podobnie jak „obiekt foo rozszerza aplikację {...}” I masz natychmiastowy dostęp do argumentów wiersza poleceń poprzez zmienną args.
AmigoNico,
scala.Enumeration (którego używasz w powyższym przykładzie kodu obiektu „WeekDay” powyżej) nie oferuje wyczerpującego dopasowania wzorca. Przebadałem wszystkie różne wzorce wyliczania obecnie stosowane w Scali i podałem je i omówiłem w tej odpowiedzi StackOverflow (w tym nowy wzorzec, który oferuje zarówno Scala.Enumeration, jak i wzorzec „sealed trait + case object”: stackoverflow. com / a / 25923651/501113
chaotic3quilibrium,
377
Muszę powiedzieć, że przykład skopiowany z powyższej dokumentacji Scali przez skaffmana ma ograniczoną użyteczność w praktyce (równie dobrze możesz użyć case objects).
Aby uzyskać coś najbardziej przypominającego Javę Enum(tj. Z rozsądnymi toStringi valueOfmetodami - być może utrwalasz wartości wyliczeniowe w bazie danych), musisz ją nieco zmodyfikować. Jeśli używałeś kodu skaffmana :
Zastąpienie @macias valueOfjest takie withName, które nie zwraca Opcji i rzuca NSE, jeśli nie ma dopasowania. Co!
Bluu
6
@Bluu Możesz dodać wartość siebie: def valueOf (name: String) = WeekDay.values.find (_. ToString == name), aby mieć opcję
centr
@centr Kiedy próbuję utworzyć a Map[Weekday.Weekday, Long]i dodać wartość powiedzieć Mondo niego, kompilator zgłasza błąd typu nieprawidłowego. Oczekiwany dzień tygodnia. Dzień tygodnia znalazł wartość? Dlaczego to się dzieje?
Sohaib,
@Sohaib Powinna to być mapa [Weekday.Value, Long].
środkowy
98
Istnieje wiele sposobów działania.
1) Użyj symboli. Nie zapewni ci jednak żadnego bezpieczeństwa, poza tym, że nie akceptujesz nie-symboli, w których spodziewany jest symbol. Wspominam o tym tutaj tylko dla kompletności. Oto przykład użycia:
def update(what:Symbol, where:Int, newValue:Array[Int]):MatrixInt=
what match{case'row=> replaceRow(where, newValue)case'col|'column=> replaceCol(where, newValue)case _ =>thrownewIllegalArgumentException}// At REPL:
scala>val a = unitMatrixInt(3)
a: teste7.MatrixInt=/100\|010|\001/
scala> a('row,1)= a.row(0)
res41: teste7.MatrixInt=/100\|100|\001/
scala> a('column,2)= a.row(0)
res42: teste7.MatrixInt=/101\|010|\000/
def update(what:Dimension, where:Int, newValue:Array[Int]):MatrixInt=
what match{caseRow=> replaceRow(where, newValue)caseColumn=> replaceCol(where, newValue)}// At REPL:
scala> a(Row,2)= a.row(1)<console>:13: error: not found: value Row
a(Row,2)= a.row(1)^
scala> a(Dimension.Row,2)= a.row(1)
res1: teste.MatrixInt=/100\|010|\010/
scala>importDimension._
importDimension._
scala> a(Row,2)= a.row(1)
res2: teste.MatrixInt=/100\|010|\010/
Niestety nie gwarantuje, że wszystkie mecze zostaną uwzględnione. Gdybym zapomniał umieścić Row lub Column w meczu, kompilator Scala nie ostrzegłby mnie. Daje mi to pewne bezpieczeństwo, ale nie tyle, ile można uzyskać.
Zastanawiasz się zatem, dlaczego kiedykolwiek używać Wyliczenia zamiast obiektów sprawy. W rzeczywistości obiekty przypadków mają wiele zalet, na przykład tutaj. Klasa Enumeration ma jednak wiele metod Collection, takich jak elementy (iterator na Scali 2.8), które zwracają Iterator, mapę, flatMap, filtr itp.
Ta odpowiedź jest zasadniczo wybranymi częściami tego artykułu na moim blogu.
„... nie akceptując symboli innych niż oczekiwany symbol”> Zgaduję, że masz na myśli, że Symbolinstancje nie mogą zawierać spacji ani znaków specjalnych. Większość osób, które spotykają się z Symbolklasą po raz pierwszy, prawdopodobnie tak uważa, ale w rzeczywistości jest to nieprawidłowe. Symbol("foo !% bar -* baz")kompiluje się i działa idealnie dobrze. Innymi słowy, możesz idealnie tworzyć Symbolinstancje owijające dowolny ciąg znaków (po prostu nie możesz tego zrobić z cukrem składniowym „pojedyncza koma”). Jedyną rzeczą, Symbolktóra gwarantuje, jest wyjątkowość każdego danego symbolu, dzięki czemu porównanie i dopasowanie jest nieznacznie szybsze.
Régis Jean-Gilles
@ RégisJean-Gilles Nie, mam na myśli, że nie można przekazać Stringna przykład argumentu Symbolparametru.
Daniel C. Sobral,
Tak, zrozumiałem tę część, ale jest to dość sporna kwestia, jeśli zastąpisz Stringinną klasą, która jest w zasadzie owijką wokół łańcucha i może być swobodnie konwertowana w obu kierunkach (jak ma to miejsce Symbol). Myślę, że właśnie to miałeś na myśli mówiąc „Nie da to żadnego bezpieczeństwa”, po prostu nie było to bardzo jasne, biorąc pod uwagę, że OP wyraźnie poprosił o bezpieczne rozwiązania typu. Nie byłem pewien, czy w momencie pisania wiedziałeś, że nie tylko nie jest to bezpieczne, ponieważ w ogóle nie są to wyliczenia, ale takżeSymbol nie gwarantuje, że przekazany argument nie będzie zawierał specjalnych znaków.
Régis Jean-Gilles
1
Aby rozwinąć, mówiąc „nie akceptując nie-symbole tam, gdzie oczekuje się symbolu”, można to rozumieć jako „nieakceptowanie wartości, które nie są instancjami Symbolu” (co jest oczywiście prawdą) lub „nieakceptowanie wartości, które nie są zwykły identyfikator podobny strun, znany jako "symbole” (co nie jest prawdą, a to błędne przekonanie, że prawie każdy ma po raz pierwszy spotykamy symbole Scala, z uwagi na fakt, że pierwsze spotkanie jest jednak specjalnej 'foonotacji który dokłada wyklucza ciągi nie będące identyfikatorami). To nieporozumienie, które chciałem rozwiać dla każdego przyszłego czytelnika.
Régis Jean-Gilles
@ RégisJean-Gilles Miałem na myśli tę pierwszą, tę, która jest oczywiście prawdą. Mam na myśli, że jest to oczywiście prawda dla każdego, kto jest przyzwyczajony do pisania statycznego. Wtedy było wiele dyskusji na temat względnych zalet pisania statycznego i „dynamicznego”, a wiele osób zainteresowanych Scalą pochodziło z dynamicznego pisania, więc pomyślałem, że nie było to oczywiste. W dzisiejszych czasach nie pomyślałbym nawet o tej uwadze. Osobiście uważam, że Symbol Scali jest brzydki i zbędny i nigdy go nie używam. Głosuję za twoim ostatnim komentarzem, ponieważ jest to dobry punkt.
Daniel C. Sobral
52
Nieco bardziej szczegółowy sposób deklarowania nazwanych wyliczeń:
Oczywiście problem polega na tym, że trzeba będzie zsynchronizować kolejność nazw i valsów, co jest łatwiejsze, jeśli nazwa i val są zadeklarowane w tym samym wierszu.
Na pierwszy rzut oka wygląda to na czystsze, ale ma tę wadę, że wymaga od opiekuna synchronizacji obu list. Na przykład dni tygodnia wydaje się mało prawdopodobne. Ale ogólnie nowa wartość może zostać wstawiona lub jedna usunięta, a dwie listy mogą nie być zsynchronizowane, w takim przypadku można wprowadzić subtelne błędy.
Brent Faust
1
Zgodnie z poprzednim komentarzem ryzyko polega na tym, że dwie różne listy mogą po cichu zsynchronizować się. Chociaż nie jest to problemem dla twojego obecnego małego przykładu, jeśli jest o wiele więcej członków (jak w dziesiątkach do setek), szanse na dwie listy cicho zsynchronizowane są znacznie wyższe. Również scala. Wyliczenia nie mogą korzystać z wyczerpującego czasu kompilacji Scala dopasowywania ostrzeżeń / błędów. Utworzyłem odpowiedź StackOverflow, która zawiera rozwiązanie sprawdzające środowisko wykonawcze w celu zapewnienia synchronizacji dwóch list: stackoverflow.com/a/25923651/501113
chaotic3quilibrium
17
Możesz użyć zapieczętowanej klasy abstrakcyjnej zamiast wyliczenia, na przykład:
Po dogłębnych badaniach wszystkich opcji dotyczących „wyliczeń” w Scali, opublikowałem znacznie pełniejszy przegląd tej domeny w innym wątku StackOverflow . Zawiera rozwiązanie wzoru „uszczelniona cecha + obiekt sprawy”, w którym rozwiązałem problem z kolejnością inicjowania klasy / obiektu JVM.
Projekt jest naprawdę dobry z przykładami i dokumentacją
Właśnie ten przykład z ich dokumentów powinien cię zainteresować
import enumeratum._
sealedtraitGreetingextendsEnumEntryobjectGreetingextendsEnum[Greeting]{/*
`findValues` is a protected method that invokes a macro to find all `Greeting` object declarations inside an `Enum`
You use it to implement the `val values` member
*/val values = findValues
caseobjectHelloextendsGreetingcaseobjectGoodByeextendsGreetingcaseobjectHiextendsGreetingcaseobjectByeextendsGreeting}// Object Greeting has a `withName(name: String)` methodGreeting.withName("Hello")// => res0: Greeting = HelloGreeting.withName("Haro")// => java.lang.IllegalArgumentException: Haro is not a member of Enum (Hello, GoodBye, Hi, Bye)// A safer alternative would be to use `withNameOption(name: String)` method which returns an Option[Greeting]Greeting.withNameOption("Hello")// => res1: Option[Greeting] = Some(Hello)Greeting.withNameOption("Haro")// => res2: Option[Greeting] = None// It is also possible to use strings case insensitivelyGreeting.withNameInsensitive("HeLLo")// => res3: Greeting = HelloGreeting.withNameInsensitiveOption("HeLLo")// => res4: Option[Greeting] = Some(Hello)// Uppercase-only strings may also be usedGreeting.withNameUppercaseOnly("HELLO")// => res5: Greeting = HelloGreeting.withNameUppercaseOnlyOption("HeLLo")// => res6: Option[Greeting] = None// Similarly, lowercase-only strings may also be usedGreeting.withNameLowercaseOnly("hello")// => res7: Greeting = HelloGreeting.withNameLowercaseOnlyOption("hello")// => res8: Option[Greeting] = Some(Hello)
Odpowiedzi:
http://www.scala-lang.org/docu/files/api/scala/Enumeration.html
Przykładowe użycie
źródło
Muszę powiedzieć, że przykład skopiowany z powyższej dokumentacji Scali przez skaffmana ma ograniczoną użyteczność w praktyce (równie dobrze możesz użyć
case object
s).Aby uzyskać coś najbardziej przypominającego Javę
Enum
(tj. Z rozsądnymitoString
ivalueOf
metodami - być może utrwalasz wartości wyliczeniowe w bazie danych), musisz ją nieco zmodyfikować. Jeśli używałeś kodu skaffmana :Podczas korzystania z następującej deklaracji:
Otrzymujesz bardziej sensowne wyniki:
źródło
valueOf
jest takiewithName
, które nie zwraca Opcji i rzuca NSE, jeśli nie ma dopasowania. Co!Map[Weekday.Weekday, Long]
i dodać wartość powiedziećMon
do niego, kompilator zgłasza błąd typu nieprawidłowego. Oczekiwany dzień tygodnia. Dzień tygodnia znalazł wartość? Dlaczego to się dzieje?Istnieje wiele sposobów działania.
1) Użyj symboli. Nie zapewni ci jednak żadnego bezpieczeństwa, poza tym, że nie akceptujesz nie-symboli, w których spodziewany jest symbol. Wspominam o tym tutaj tylko dla kompletności. Oto przykład użycia:
2) Korzystanie z klasy
Enumeration
:lub, jeśli chcesz go serializować lub wyświetlić:
Można to wykorzystać w następujący sposób:
Niestety nie gwarantuje, że wszystkie mecze zostaną uwzględnione. Gdybym zapomniał umieścić Row lub Column w meczu, kompilator Scala nie ostrzegłby mnie. Daje mi to pewne bezpieczeństwo, ale nie tyle, ile można uzyskać.
3) Obiekty sprawy:
Teraz, jeśli pominę sprawę
match
, kompilator ostrzeże mnie:Jest używany prawie w ten sam sposób i nawet nie potrzebuje
import
:Zastanawiasz się zatem, dlaczego kiedykolwiek używać Wyliczenia zamiast obiektów sprawy. W rzeczywistości obiekty przypadków mają wiele zalet, na przykład tutaj. Klasa Enumeration ma jednak wiele metod Collection, takich jak elementy (iterator na Scali 2.8), które zwracają Iterator, mapę, flatMap, filtr itp.
Ta odpowiedź jest zasadniczo wybranymi częściami tego artykułu na moim blogu.
źródło
Symbol
instancje nie mogą zawierać spacji ani znaków specjalnych. Większość osób, które spotykają się zSymbol
klasą po raz pierwszy, prawdopodobnie tak uważa, ale w rzeczywistości jest to nieprawidłowe.Symbol("foo !% bar -* baz")
kompiluje się i działa idealnie dobrze. Innymi słowy, możesz idealnie tworzyćSymbol
instancje owijające dowolny ciąg znaków (po prostu nie możesz tego zrobić z cukrem składniowym „pojedyncza koma”). Jedyną rzeczą,Symbol
która gwarantuje, jest wyjątkowość każdego danego symbolu, dzięki czemu porównanie i dopasowanie jest nieznacznie szybsze.String
na przykład argumentuSymbol
parametru.String
inną klasą, która jest w zasadzie owijką wokół łańcucha i może być swobodnie konwertowana w obu kierunkach (jak ma to miejsceSymbol
). Myślę, że właśnie to miałeś na myśli mówiąc „Nie da to żadnego bezpieczeństwa”, po prostu nie było to bardzo jasne, biorąc pod uwagę, że OP wyraźnie poprosił o bezpieczne rozwiązania typu. Nie byłem pewien, czy w momencie pisania wiedziałeś, że nie tylko nie jest to bezpieczne, ponieważ w ogóle nie są to wyliczenia, ale takżeSymbol
nie gwarantuje, że przekazany argument nie będzie zawierał specjalnych znaków.'foo
notacji który dokłada wyklucza ciągi nie będące identyfikatorami). To nieporozumienie, które chciałem rozwiać dla każdego przyszłego czytelnika.Nieco bardziej szczegółowy sposób deklarowania nazwanych wyliczeń:
Oczywiście problem polega na tym, że trzeba będzie zsynchronizować kolejność nazw i valsów, co jest łatwiejsze, jeśli nazwa i val są zadeklarowane w tym samym wierszu.
źródło
Możesz użyć zapieczętowanej klasy abstrakcyjnej zamiast wyliczenia, na przykład:
źródło
właśnie odkryłem enumeratum . jest niesamowity i równie niesamowity, nie jest bardziej znany!
źródło
Po dogłębnych badaniach wszystkich opcji dotyczących „wyliczeń” w Scali, opublikowałem znacznie pełniejszy przegląd tej domeny w innym wątku StackOverflow . Zawiera rozwiązanie wzoru „uszczelniona cecha + obiekt sprawy”, w którym rozwiązałem problem z kolejnością inicjowania klasy / obiektu JVM.
źródło
Dotty (Scala 3) będzie obsługiwał natywne wyliczenia. Sprawdź tutaj i tutaj .
źródło
W Scali jest bardzo wygodnie z https://github.com/lloydmeta/enumeratum
Projekt jest naprawdę dobry z przykładami i dokumentacją
Właśnie ten przykład z ich dokumentów powinien cię zainteresować
źródło