Czy błędem jest używanie flag do wyliczania „grupowania”?

12

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 SomeAttributeswartość może być kombinacją Foo, Bari 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 Declarationnie może być jednocześnie Variablea LibraryProcedure- dwie indywidualne wartości nie mogą być łączone .. i nie są.

Chociaż te flagi są niezwykle przydatne (bardzo łatwo jest zweryfikować, czy dana DeclarationTypejest a Propertylub 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 DeclarationTypewystawiony w Declarationklasie bazowej : posiadanie PropertyTypewyliczenia 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?

Mathieu Guindon
źródło
Zastanów się, jak ludzie będą używać obiektów typu DeclarationType. Jeśli chcę ustalić, czy xpodtyp jest, czy nie y, prawdopodobnie będę chciał napisać to jako x.IsSubtypeOf(y), a nie jako x && y == y.
Tanner Swett,
1
@TannerSwett To jest dokładnie to, co x.HasFlag(DeclarationType.Member)robi ....
Mathieu Guindon
Tak to prawda. Ale jeśli twoja metoda jest wywoływana HasFlagzamiast IsSubtypeOf, 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 ma IsSubtypeOfjako prawdziwą metodę.
Tanner Swett,

Odpowiedzi:

10

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 Memberdziedziczy po DeclarationType, Propertydziedziczy po Memberi 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+ enumjako „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

JacquesB
źródło
Dziedziczenie jest dobrym pomysłem, ale twoja odpowiedź sugeruje klasę na wyliczenie, co wydaje się przesadą.
Frank Hileman,
@FrankHileman: „Liście” w hierarchii mogą być reprezentowane jako wartości wyliczane, a nie klasy, ale nie jestem pewien, czy byłoby lepiej. Zależy, czy różne zachowania byłyby powiązane z różnymi wartościami wyliczenia, w którym to przypadku lepsze byłyby odrębne klasy.
JacquesB,
4
Klasyfikacja nie jest hierarchiczna, kombinacje są wstępnie zdefiniowanymi stanami. Wykorzystanie do tego dziedziczenia byłoby naprawdę nadużyciem, czyli nadużyciem klas. Z klasą oczekuję przynajmniej niektórych danych lub pewnego zachowania. Wiązka zajęć bez któregoś z nich to… racja, wyliczenie.
Martin Maat,
O tym łączu do mojego repozytorium: zachowanie dla poszczególnych typów ma więcej wspólnego z ParserRuleContexttypem generowanej klasy niż z wyliczeniem. Ten kod próbuje uzyskać pozycję tokena, w której należy wstawić As {Type}klauzulę w deklaracji; te ParserRuleContextgenerowane 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ć ich partial-ness, aby zmusić je do implementacji interfejsów, np. zmusić ich do ujawnienia jakiejś AsTypeClauseTokenPositionwłasności .. dużo pracy.
Mathieu Guindon
6

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:

  1. Moje główne zastrzeżenie polega na tym, że nie ma sposobu na egzekwowanie tego:

    Oczywiście dana Deklaracja nie może być zarówno Zmienną, jak i Procedurą Biblioteczną - dwóch indywidualnych wartości nie można łączyć .. i nie są.

    Chociaż nie deklarujesz powyższej kombinacji, ten kod

    var oops = DeclarationType.Variable | DeclarationType.LibraryProcedure;

    Jest całkowicie poprawny. I nie ma sposobu, aby złapać te błędy w czasie kompilacji.

  2. 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, inta 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.

Nikita B.
źródło
Tak więc dokładne testy jednostki parsera zajęłyby się wtedy numerem 1. FWIW enum zaczęło się jako standardowe wyliczanie bez flag około 3 lata temu. Po zmęczeniu ciągłym sprawdzaniem pewnych konkretnych wartości (powiedzmy podczas inspekcji kodu) pojawiły się flagi enum. Z czasem lista też nie będzie się powiększać, ale tak naprawdę to nie intlada wyzwanie.
Mathieu Guindon
3

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/ isczekach). 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 jak instanceof/ isczek.

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ńcucha instanceofw 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ć Namewłaściwość), istnieje wiele bogatych zachowań polimorficznych wspólnych dla pobliskiego rodzeństwa. Na przykład Functioni Procedureprawdopodobnie dzielą niektóre zachowania (oba są wywoływalne i akceptują listę wpisanych argumentów wejściowych) i na PropertyGetpewno odziedziczą zachowania Function(oba mają a ReturnType). 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/ iskontroli. Wydajność nie jest jednym z powodów. Powodem jest raczej uniemożliwienie programistom organicznego odkrycia odpowiednich zachowań polimorficznych, tak jakby instanceof/ isbył 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 ...

[Flags]
public enum DeclarationType
{
    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,
    LibraryFunction = 1 << 23 | Function,
    LibraryProcedure = 1 << 24 | Procedure,
}

do tej zmodyfikowanej wersji:

[Flags]
public enum DeclarationType
{
    Nothing = 0, // to facilitate bit testing

    // Let's assume Member is not a concrete thing, 
    // which means it doesn't need its own bit
    /* Member = 1 << 7, */

    // Procedure and Function are concrete things; meanwhile 
    // they can still have sub-types.
    Procedure = 1 << 8, 
    Function = 1 << 9, 
    Property = 1 << 10,

    PropertyGet = 1 << 11,
    PropertyLet = 1 << 12,
    PropertySet = 1 << 13,

    LibraryFunction = 1 << 23,
    LibraryProcedure = 1 << 24,

    // new
    Procedures = Procedure | PropertyLet | PropertySet | LibraryProcedure,
    Functions = Function | PropertyGet | LibraryFunction,
    Properties = PropertyGet | PropertyLet | PropertySet,
    Members = Procedures | Functions | Properties,
    LibraryMembers = LibraryFunction | LibraryProcedure 
}

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 Propertyjest początkowo identyfikatorem (gdy widzisz tylko jego nazwę, nie widząc, jak jest używany w kodzie), ale może przekształcić się w PropertyGet/ PropertyLet/, PropertySetjak tylko zobaczysz, jak jest używany w kodzie. Innymi słowy, na różnych etapach analizy może być konieczne oznaczenie Propertyidentyfikatora 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.

public enum DeclarationType
{
    Procedure = 8,
    Function = 9,
    Property = 10,
    PropertyGet = 11,
    PropertyLet = 12,
    PropertySet = 13,
    LibraryFunction = 23,
    LibraryProcedure = 24,
}

static readonly bool[] DeclarationTypeIsMember = new bool[32]
{
    ?, ?, ?, ?, ?, ?, ?, ?,                   // bit[0] ... bit[7]
    true, true, true, true, true, true, ?, ?, // bit[8] ... bit[15]
    ?, ?, ?, ?, ?, ?, ?, true,                // bit[16] ... bit[23]
    true, ...                                 // bit[24] ... 
}

static bool IsMember(DeclarationType dt)
{
    int intValue = (int)dt;
    return (intValue < 0 || intValue >= 32) ? false : DeclarationTypeIsMember[intValue];
    // you can also throw an exception if the enum is outside range.
}

// likewise for IsFunction(dt), IsProcedure(dt), IsProperty(dt), ...

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 Typezamiast 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:

// For disambiguation, I'll assume that the actual 
// (behavior-implementing) classes are under the 
// "Lang" namespace.

static readonly Dictionary<Type, DeclarationType> TypeToDeclEnum = ... 
{
    { typeof(Lang.Procedure), DeclarationType.Procedure },
    { typeof(Lang.Function), DeclarationType.Function },
    { typeof(Lang.Property), DeclarationType.Property },
    ...
};

static readonly Dictionary<DeclarationType, Type> DeclEnumToType = ...
{
    // same as the first dictionary; 
    // just swap the key and the value
    ...
};

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:

  • Najpierw zaprojektuj (lub popraw) hierarchię dziedziczenia,
  • Następnie wróć i zaprojektuj swoje wyliczenia, aby zbliżyć się do hierarchii dziedziczenia.
rwong
źródło
Projekt jest właściwie dodatkiem VBIDE - Analizuję i analizuję kod VBA =)
Mathieu Guindon
1

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!

Martin Maat
źródło
4
thingy is Fruitjest bardziej czytelny niż oba.
JacquesB,