Rozumiem, że [Flag]
wyliczenia są zwykle używane do rzeczy, które można łączyć, w których poszczególne wartości nie wykluczają się wzajemnie .
Na przykład:
[Flags]
public enum SomeAttributes
{
Foo = 1 << 0,
Bar = 1 << 1,
Baz = 1 << 2,
}
Jeżeli jakakolwiek SomeAttributes
wartość może być kombinacją Foo
, Bar
i Baz
.
W bardziej skomplikowanym scenariuszu z życia używam wyliczenia, aby opisać DeclarationType
:
[Flags]
public enum DeclarationType
{
Project = 1 << 0,
Module = 1 << 1,
ProceduralModule = 1 << 2 | Module,
ClassModule = 1 << 3 | Module,
UserForm = 1 << 4 | ClassModule,
Document = 1 << 5 | ClassModule,
ModuleOption = 1 << 6,
Member = 1 << 7,
Procedure = 1 << 8 | Member,
Function = 1 << 9 | Member,
Property = 1 << 10 | Member,
PropertyGet = 1 << 11 | Property | Function,
PropertyLet = 1 << 12 | Property | Procedure,
PropertySet = 1 << 13 | Property | Procedure,
Parameter = 1 << 14,
Variable = 1 << 15,
Control = 1 << 16 | Variable,
Constant = 1 << 17,
Enumeration = 1 << 18,
EnumerationMember = 1 << 19,
Event = 1 << 20,
UserDefinedType = 1 << 21,
UserDefinedTypeMember = 1 << 22,
LibraryFunction = 1 << 23 | Function,
LibraryProcedure = 1 << 24 | Procedure,
LineLabel = 1 << 25,
UnresolvedMember = 1 << 26,
BracketedExpression = 1 << 27,
ComAlias = 1 << 28
}
Oczywiście dana Declaration
nie może być jednocześnie Variable
a LibraryProcedure
- dwie indywidualne wartości nie mogą być łączone .. i nie są.
Chociaż te flagi są niezwykle przydatne (bardzo łatwo jest zweryfikować, czy dana DeclarationType
jest a Property
lub a Module
), wydaje się „złe”, ponieważ flagi nie są tak naprawdę używane do łączenia wartości, ale raczej do grupowania ich w „podtypy”.
Powiedziano mi, że jest to nadużycie flagi enum - ta odpowiedź zasadniczo mówi, że jeśli mam zestaw wartości mających zastosowanie do jabłek i inny zestaw mający zastosowanie do pomarańczy, to potrzebuję innego rodzaju wyliczenia dla jabłek i innego dla pomarańczy - Problem polega na tym, że potrzebuję wszystkich deklaracji, aby mieć wspólny interfejs, ponieważ jestem DeclarationType
wystawiony w Declaration
klasie bazowej : posiadanie PropertyType
wyliczenia nie byłoby w ogóle przydatne.
Czy to niechlujny / zaskakujący / obraźliwy projekt? Jeśli tak, to jak zazwyczaj rozwiązuje się ten problem?
źródło
DeclarationType
. Jeśli chcę ustalić, czyx
podtyp jest, czy niey
, prawdopodobnie będę chciał napisać to jakox.IsSubtypeOf(y)
, a nie jakox && y == y
.x.HasFlag(DeclarationType.Member)
robi ....HasFlag
zamiastIsSubtypeOf
, to potrzebuję innego sposobu, aby dowiedzieć się, że to, co tak naprawdę znaczy, to „podtyp”. Możesz stworzyć metodę rozszerzenia, ale jako użytkownik nie dziwi mnie to,DeclarationType
że jest strukturą, która maIsSubtypeOf
jako prawdziwą metodę.Odpowiedzi:
To zdecydowanie nadużywa wyliczeń i flag! Może ci się to przydać, ale każdy, kto przeczyta kod, będzie bardzo zdezorientowany.
Jeśli dobrze rozumiem, masz hierarchiczną klasyfikację deklaracji. Jest to zbyt wiele informacji do zakodowania w jednym wyliczeniu. Ale istnieje oczywista alternatywa: używaj klas i dziedziczenia! Więc
Member
dziedziczy poDeclarationType
,Property
dziedziczy poMember
i tak dalej.Wyliczenia są odpowiednie w niektórych szczególnych okolicznościach: jeśli wartość jest zawsze jedną z ograniczonej liczby opcji lub jeśli jest to dowolna kombinacja ograniczonej liczby opcji (flag). Wszelkie informacje, które są bardziej złożone lub ustrukturyzowane, powinny być reprezentowane za pomocą obiektów.
Edycja : W twoim „prawdziwym scenariuszu” wydaje się, że istnieje wiele miejsc, w których zachowanie jest wybierane w zależności od wartości wyliczenia. To naprawdę antypattern, ponieważ używasz
switch
+enum
jako „słabego polimorfizmu człowieka”. Po prostu zmień wartość wyliczenia na odrębne klasy, które zawierają zachowanie specyficzne dla deklaracji, a Twój kod będzie znacznie czystszyźródło
ParserRuleContext
typem generowanej klasy niż z wyliczeniem. Ten kod próbuje uzyskać pozycję tokena, w której należy wstawićAs {Type}
klauzulę w deklaracji; teParserRuleContext
generowane klasy są generowane przez Antr4 na gramatykę, która definiuje reguły parsera - tak naprawdę nie kontroluję hierarchii dziedziczenia [raczej płytkich] węzłów drzewa, chociaż mogę wykorzystać ichpartial
-ness, aby zmusić je do implementacji interfejsów, np. zmusić ich do ujawnienia jakiejśAsTypeClauseTokenPosition
własności .. dużo pracy.Uważam to podejście za wystarczająco łatwe do odczytania i zrozumienia. IMHO, nie należy się tym mylić. Biorąc to pod uwagę, mam pewne obawy dotyczące tego podejścia:
Moje główne zastrzeżenie polega na tym, że nie ma sposobu na egzekwowanie tego:
Chociaż nie deklarujesz powyższej kombinacji, ten kod
Jest całkowicie poprawny. I nie ma sposobu, aby złapać te błędy w czasie kompilacji.
Istnieje limit ilości informacji, które można zakodować w flagach bitowych, czyli 64 bitów? Na razie zbliżasz się niebezpiecznie do rozmiaru,
int
a jeśli ten enum nadal rośnie, możesz w końcu po prostu skończyć się trochę ...Podsumowując, myślę, że jest to prawidłowe podejście, ale wahałbym się użyć go do dużych / złożonych hierarchii flag.
źródło
int
lada wyzwanie.TL; DR Przewiń na sam dół.
Z tego, co widzę, wdrażasz nowy język na C #. Wyliczenia wydają się oznaczać typ identyfikatora (lub cokolwiek, co ma nazwę i które pojawia się w kodzie źródłowym nowego języka), który wydaje się być stosowany do węzłów, które mają zostać dodane do drzewnej reprezentacji programu.
W tej szczególnej sytuacji istnieje bardzo niewiele zachowań polimorficznych między różnymi typami węzłów. Innymi słowy, chociaż konieczne jest, aby drzewo mogło zawierać węzły bardzo różnych typów (wariantów), faktyczna wizyta tych węzłów będzie zasadniczo polegać na gigantycznym łańcuchu „jeśli to wtedy” (lub
instanceof
/is
czekach). Te gigantyczne kontrole prawdopodobnie będą miały miejsce w wielu różnych miejscach projektu. To jest powód, dla którego wyliczenia mogą wydawać się pomocne, a przynajmniej są tak pomocne jakinstanceof
/is
czek.Wzorzec odwiedzających może być nadal przydatny. Innymi słowy, istnieją różne style kodowania, których można użyć zamiast gigantycznego łańcucha
instanceof
. Jeśli jednak chcesz omówić różne zalety i wady, wolisz zaprezentować przykład kodu z najbrzydszego łańcuchainstanceof
w projekcie, zamiast spierać się o wyliczenia.Nie oznacza to, że klasy i hierarchia dziedziczenia nie są przydatne. Wręcz przeciwnie. Chociaż nie ma żadnych zachowań polimorficznych, które działałyby dla każdego typu deklaracji (poza tym, że każda deklaracja musi mieć
Name
właściwość), istnieje wiele bogatych zachowań polimorficznych wspólnych dla pobliskiego rodzeństwa. Na przykładFunction
iProcedure
prawdopodobnie dzielą niektóre zachowania (oba są wywoływalne i akceptują listę wpisanych argumentów wejściowych) i naPropertyGet
pewno odziedziczą zachowaniaFunction
(oba mają aReturnType
). Możesz użyć wyliczeń lub kontroli dziedziczenia dla gigantycznego łańcucha „jeśli-to-inaczej”, ale zachowania polimorficzne, jakkolwiek fragmentaryczne, muszą być nadal implementowane w klasach.Istnieje wiele internetowych porad dotyczących nadużywania
instanceof
/is
kontroli. Wydajność nie jest jednym z powodów. Powodem jest raczej uniemożliwienie programistom organicznego odkrycia odpowiednich zachowań polimorficznych, tak jakbyinstanceof
/is
był kulą. Ale w twojej sytuacji nie masz innego wyboru, ponieważ te węzły mają bardzo niewiele wspólnego.Oto kilka konkretnych sugestii.
Istnieje kilka sposobów reprezentowania grup nie będących liśćmi.
Porównaj następujący fragment oryginalnego kodu ...
do tej zmodyfikowanej wersji:
Ta zmodyfikowana wersja unika przydzielania bitów do nieprecyzyjnych typów deklaracji. Zamiast tego nieokreślone typy deklaracji (abstrakcyjne grupy typów deklaracji) mają po prostu wartości wyliczeniowe, które są bitowe lub (suma bitów) wszystkich swoich potomków.
Jest zastrzeżenie: jeśli istnieje abstrakcyjny typ deklaracji, który ma jedno dziecko, a jeśli istnieje potrzeba rozróżnienia między abstrakcyjnym (rodzicem) a konkretnym (dzieckiem), wówczas abstrakcyjny nadal będzie potrzebował własnego bitu .
Jedno zastrzeżenie, które jest specyficzne dla tego pytania: a
Property
jest początkowo identyfikatorem (gdy widzisz tylko jego nazwę, nie widząc, jak jest używany w kodzie), ale może przekształcić się wPropertyGet
/PropertyLet
/,PropertySet
jak tylko zobaczysz, jak jest używany w kodzie. Innymi słowy, na różnych etapach analizy może być konieczne oznaczenieProperty
identyfikatora jako „ta nazwa odnosi się do właściwości”, a później zmiana tego na „ten wiersz kodu uzyskuje dostęp do tej właściwości w określony sposób”.Aby rozwiązać ten problem, możesz potrzebować dwóch zestawów wyliczeń; jedno wyliczenie oznacza, co to jest nazwa (identyfikator); inne wyliczenie oznacza, co kod próbuje zrobić (np. deklarując treść czegoś; próbując użyć czegoś w określony sposób).
Zastanów się, czy zamiast tego można odczytać informacje pomocnicze o każdej wartości wyliczenia z tablicy.
Ta sugestia wyklucza się wzajemnie z innymi sugestiami, ponieważ wymaga zamiany potęg dwóch wartości z powrotem na małe nieujemne wartości całkowite.
Utrzymanie będzie problematyczne.
Sprawdź, czy mapowanie jeden na jeden między typami C # (klasy w hierarchii dziedziczenia) i wartościami wyliczania.
(Alternatywnie możesz dostosować wartości wyliczeniowe, aby zapewnić mapowanie typu jeden do jednego z typami).
W języku C # wiele bibliotek nadużywa
Type object.GetType()
dobrej i złej metody.Gdziekolwiek przechowujesz wyliczenie jako wartość, możesz zadać sobie pytanie, czy
Type
zamiast tego możesz zapisać wartość jako wartość.Aby użyć tej sztuczki, możesz zainicjować dwie tabele skrótów tylko do odczytu, a mianowicie:
Ostateczne potwierdzenie dla osób sugerujących klasy i hierarchię dziedziczenia ...
Gdy zobaczysz, że wyliczenia są przybliżeniem do hierarchii dziedziczenia , obowiązuje następująca rada:
źródło
Uważam, że użycie flag jest naprawdę inteligentne, kreatywne, eleganckie i potencjalnie najbardziej wydajne. Nie mam też żadnego problemu z jej odczytaniem.
Flagi są sposobem sygnalizowania stanu kwalifikacji. Jeśli chcę wiedzieć, czy coś jest owocem, znajduję
thingy & Organic.Fruit! = 0
bardziej czytelny niż
thingy & (Organic.Apple | Organic.Orange | Organic.Pear)! = 0
Celem enum Flag jest umożliwienie łączenia wielu stanów. Właśnie uczyniłeś to bardziej użytecznym i czytelnym. W swoim kodzie przekazujesz pojęcie owoców, nie muszę się domyślać, że jabłko, pomarańcza i gruszka oznaczają owoce.
Daj temu facetowi kilka punktów brownie!
źródło
thingy is Fruit
jest bardziej czytelny niż oba.