Ist es falsch, Flags zum Gruppieren von Enums zu verwenden?

12

Ich verstehe, dass [Flag]Aufzählungen normalerweise für Dinge verwendet werden, die kombiniert werden können, wobei sich die einzelnen Werte nicht gegenseitig ausschließen .

Beispielsweise:

[Flags]
public enum SomeAttributes
{
    Foo = 1 << 0,
    Bar = 1 << 1,
    Baz = 1 << 2,
}

Wo jeder SomeAttributesWert eine Kombination von Foo, Barund sein kann Baz.

In einem komplizierteren, realistischen Szenario benutze ich eine Aufzählung, um Folgendes zu beschreiben 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
}

Offensichtlich kann eine gegebene DeclarationZahl nicht gleichzeitig a Variableund a sein LibraryProcedure- die beiden Einzelwerte können nicht kombiniert werden. Und sie sind es auch nicht.

Diese Flags sind zwar äußerst nützlich (es ist sehr einfach zu überprüfen, ob ein gegebenes DeclarationTypea Propertyoder a ist Module), sie fühlen sich jedoch "falsch" an, da die Flags nicht wirklich zum Kombinieren von Werten, sondern zum Gruppieren in "Untertypen" verwendet werden.

Mir wurde gesagt, dass dies Enum-Flags missbraucht - diese Antwort besagt im Wesentlichen, dass ich für Äpfel einen anderen Enum-Typ und für Orangen einen anderen Enum-Typ benötige - die Das Problem dabei ist, dass alle Deklarationen eine gemeinsame Schnittstelle haben müssen, damit DeclarationTypesie in der Basisklasse Declarationverfügbar gemacht werden können: Eine PropertyTypeAufzählung wäre überhaupt nicht nützlich.

Ist das ein schlampiges / überraschendes / missbräuchliches Design? Wenn ja, wie wird dieses Problem normalerweise gelöst?

Mathieu Guindon
quelle
Überlegen Sie, wie Benutzer Objekte vom Typ verwenden DeclarationType. Wenn ich herausfinden will, ob es sich xum einen Untertyp handelt y, möchte ich das wahrscheinlich als schreiben x.IsSubtypeOf(y), nicht als x && y == y.
Tanner Swett
1
@ TannerSwett Das ist ganz genau das, was x.HasFlag(DeclarationType.Member)....
Mathieu Guindon
Ja das stimmt. Aber wenn Ihre Methode HasFlagstatt aufgerufen wird IsSubtypeOf, muss ich auf andere Weise herausfinden, dass das, was es wirklich bedeutet, "Subtyp von" ist. Sie könnten eine Erweiterungsmethode erstellen, aber als Benutzer würde es mich am wenigsten überraschen DeclarationType, nur eine Struktur zu sein, die IsSubtypeOfeine echte Methode darstellt.
Tanner Swett

Antworten:

10

Dies missbraucht definitiv Aufzählungen und Flaggen! Es könnte für Sie funktionieren, aber jeder andere, der den Code liest, wird stark verwirrt sein.

Wenn ich das richtig verstehe, haben Sie eine hierarchische Klassifizierung der Deklarationen. Dies sind viel zu viele Informationen, um sie in einer einzigen Aufzählung zu kodieren. Aber es gibt eine offensichtliche Alternative: Verwenden Sie Klassen und Vererbung! Also Membererbt von DeclarationType, Propertyerbt von Memberund so weiter.

Aufzählungen sind unter bestimmten Umständen angebracht: Wenn ein Wert immer einer begrenzten Anzahl von Optionen entspricht oder wenn es sich um eine beliebige Kombination einer begrenzten Anzahl von Optionen (Flags) handelt. Informationen, die komplexer oder strukturierter sind, sollten mithilfe von Objekten dargestellt werden.

Bearbeiten : In Ihrem "realen Szenario" scheint es mehrere Stellen zu geben, an denen das Verhalten abhängig vom Wert der Aufzählung ausgewählt wird. Dies ist wirklich ein Gegenmuster, da Sie switch+ enumals "Polymorphismus des armen Mannes" verwenden. Verwandeln Sie den Enum-Wert einfach in verschiedene Klassen, die das deklarationsspezifische Verhalten einschließen, und Ihr Code wird viel sauberer

JacquesB
quelle
Vererbung ist eine gute Idee, aber Ihre Antwort impliziert eine Klasse pro Aufzählung, die übertrieben zu sein scheint.
Frank Hileman
@FrankHileman: Die "Blätter" in der Hierarchie könnten als Aufzählungswerte und nicht als Klassen dargestellt werden, aber ich bin nicht sicher, ob es besser wäre. Hängt davon ab, ob mit den verschiedenen Aufzählungswerten ein unterschiedliches Verhalten verbunden ist. In diesem Fall sind unterschiedliche Klassen besser.
JacquesB
4
Die Klassifizierung ist nicht hierarchisch, die Kombinationen sind vordefinierte Zustände. Vererbung dafür zu verwenden, wäre wirklich Missbrauch, das heißt Missbrauch von Klassen. Bei einer Klasse erwarte ich zumindest einige Daten oder Verhaltensweisen. Ein Haufen von Klassen ohne beides ist ... richtig, eine Aufzählung.
Martin Maat
Zu diesem Link zu meinem Repo: Das Verhalten pro Typ hat mehr mit dem ParserRuleContextTyp der generierten Klasse zu tun als mit der Aufzählung. Dieser Code versucht, die Token-Position abzurufen, an der eine As {Type}Klausel in die Deklaration eingefügt werden soll. Diese ParserRuleContextabgeleiteten Klassen werden von Antr4 anhand einer Grammatik generiert, die die Parserregeln definiert. Ich kontrolliere die [eher flache] Vererbungshierarchie der Analysebaumknoten nicht wirklich, obwohl ich ihre partialFähigkeit nutzen kann, um Schnittstellen zu implementieren, z machen sie etwas AsTypeClauseTokenPositionEigentum aussetzen .. viel Arbeit.
Mathieu Guindon
6

Ich finde diesen Ansatz leicht zu lesen und zu verstehen. IMHO, es ist nicht verwirrend. Davon abgesehen habe ich einige Bedenken hinsichtlich dieses Ansatzes:

  1. Mein Hauptvorbehalt ist, dass es keine Möglichkeit gibt, dies durchzusetzen:

    Offensichtlich kann eine bestimmte Deklaration nicht gleichzeitig eine Variable und ein LibraryProcedure sein - die beiden Einzelwerte können nicht kombiniert werden.

    Während Sie die obige Kombination nicht deklarieren, ist dieser Code

    var oops = DeclarationType.Variable | DeclarationType.LibraryProcedure;

    Ist völlig gültig. Und es gibt keine Möglichkeit, diese Fehler beim Kompilieren zu erfassen.

  2. Es gibt eine Begrenzung für die Menge an Informationen, die Sie in Bit-Flags codieren können. Was sind 64 Bits? Im intMoment nähern Sie sich in gefährlicher Weise der Größe von und wenn diese Enumeration weiter zunimmt, werden Ihnen möglicherweise irgendwann die Teile ausgehen ...

Fazit ist, ich denke, dies ist ein gültiger Ansatz, aber ich würde zögern, ihn für große / komplexe Hierarchien von Flags zu verwenden.

Nikita B
quelle
So gründliche Parser-Unit-Tests würden dann die Nummer 1 ansprechen. FWIW die Aufzählung begann vor etwa 3 Jahren als Standard-No-Flags-Aufzählung. Es wurde langsam müde, immer wieder nach bestimmten Werten zu suchen (zum Beispiel bei Code-Inspektionen), dass Aufzählungs-Flags auftauchten. Die Liste wird mit der Zeit auch nicht wirklich wachsen, aber die Schlagkraft intist trotzdem ein echtes Problem.
Mathieu Guindon
3

TL; DR Nach ganz unten scrollen.


Soweit ich weiß, implementieren Sie eine neue Sprache auf C #. Die Aufzählungen scheinen den Typ eines Bezeichners (oder alles, was einen Namen hat und im Quellcode der neuen Sprache erscheint) zu bezeichnen, der auf Knoten angewendet wird, die einer Baumdarstellung des Programms hinzugefügt werden sollen.

In dieser besonderen Situation gibt es sehr wenige polymorphe Verhaltensweisen zwischen den verschiedenen Knotentypen. Mit anderen Worten, während es erforderlich ist, dass der Baum Knoten sehr unterschiedlicher Typen (Varianten) enthalten kann, greift der tatsächliche Besuch dieser Knoten im Wesentlichen auf eine riesige Wenn-Dann-Sonst-Kette (oder instanceof/ isSchecks) zurück. Diese riesigen Kontrollen werden wahrscheinlich an vielen verschiedenen Stellen im gesamten Projekt stattfinden. Dies ist der Grund, warum Aufzählungen hilfreich erscheinen oder mindestens so hilfreich sind wie instanceof/ ischecks.

Das Besuchermuster kann dennoch nützlich sein. Mit anderen Worten, es gibt verschiedene Codierungsstile, die anstelle der riesigen Kette von verwendet werden können instanceof. Wenn Sie sich jedoch mit den verschiedenen Vor- und Nachteilen instanceofbefassen möchten , hätten Sie sich dafür entschieden, ein Codebeispiel aus der hässlichsten Kette des Projekts vorzustellen, anstatt über Aufzählungen zu streiten.

Dies bedeutet nicht, dass Klassen und Vererbungshierarchien nicht nützlich sind. Ganz im Gegenteil. Während es keine polymorphen Verhaltensweisen gibt, die für jeden Deklarationstyp gelten (abgesehen von der Tatsache, dass jede Deklaration eine NameEigenschaft haben muss ), gibt es viele polymorphe Verhaltensweisen, die von Geschwistern in der Nähe geteilt werden. Zum Beispiel Functionund Procedurewahrscheinlich teilen sie einige Verhaltensweisen (beide sind aufrufbar und akzeptieren eine Liste typisierter Eingabeargumente) und PropertyGeterben definitiv Verhaltensweisen von Function(beide haben ein ReturnType). Sie können entweder Aufzählungen oder Vererbungsprüfungen für die riesige Wenn-Dann-Sonst-Kette verwenden, aber das polymorphe Verhalten, wie fragmentiert es auch sein mag, muss immer noch in Klassen implementiert werden.

Es gibt viele Online-Ratschläge gegen übermäßigen Gebrauch von instanceof/ isSchecks. Leistung ist nicht einer der Gründe. Vielmehr ist der Grund , den Programmierer zu verhindern , dass organisch geeignetes polymorphes Verhalten zu entdecken, als ob instanceof/ iseine Krücke ist. In Ihrer Situation haben Sie jedoch keine andere Wahl, da diese Knoten nur sehr wenig gemeinsam haben.

Hier nun einige konkrete Vorschläge.


Es gibt verschiedene Möglichkeiten, die Nicht-Blatt-Gruppierungen darzustellen.


Vergleichen Sie den folgenden Auszug aus Ihrem Originalcode ...

[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,
}

zu dieser modifizierten Version:

[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 
}

Diese modifizierte Version vermeidet die Zuweisung von Bits zu nicht konkreten Deklarationstypen. Stattdessen haben nicht konkrete Deklarationstypen (abstrakte Gruppierungen von Deklarationstypen) einfach Aufzählungswerte, die das bitweise Oder (Vereinigung der Bits) über alle untergeordneten Typen darstellen.

Es gibt eine Einschränkung: Wenn es einen abstrakten Deklarationstyp gibt, der ein einzelnes untergeordnetes Element hat, und wenn zwischen dem abstrakten (übergeordneten) und dem konkreten (untergeordneten) Element unterschieden werden muss, benötigt das abstrakte Element immer noch ein eigenes Element .


Eine Einschränkung , die spezifisch auf diese Frage ist: a Propertyist zunächst eine Kennung (wenn man nur seinen Namen, ohne zu sehen , wie es im Code verwendet wird), aber es kann umwandeln in PropertyGet/ PropertyLet/ PropertySet, sobald Sie sehen , wie es verwendet wird im Code. Mit anderen Worten, in verschiedenen Phasen des Parsens müssen Sie entweder einen PropertyBezeichner als "dieser Name bezieht sich auf eine Eigenschaft" markieren und später in "diese Codezeile greift auf diese Eigenschaft auf eine bestimmte Weise zu" ändern.

Um diese Einschränkung zu beheben, benötigen Sie möglicherweise zwei Sätze von Aufzählungen. Eine Aufzählung gibt an, was ein Name (Bezeichner) ist. Eine andere Aufzählung gibt an, was der Code versucht zu tun (z. B. den Körper von etwas zu deklarieren; zu versuchen, etwas auf eine bestimmte Weise zu verwenden).


Überlegen Sie, ob die Zusatzinformationen zu den einzelnen Aufzählungswerten stattdessen aus einem Array gelesen werden können.

Dieser Vorschlag schließt sich mit anderen Vorschlägen gegenseitig aus, da die Umwandlung von Zweierpotenzen in kleine nicht negative ganzzahlige Werte erforderlich ist.

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), ...

Die Wartbarkeit wird problematisch sein.


Überprüfen Sie, ob eine Eins-zu-Eins-Zuordnung zwischen C # -Typen (Klassen in einer Vererbungshierarchie) und Ihren Aufzählungswerten besteht.

(Alternativ können Sie Ihre Enum-Werte optimieren, um eine Eins-zu-Eins-Zuordnung mit Typen sicherzustellen.)

In C # missbrauchen viele Bibliotheken die raffinierte Type object.GetType()Methode für gut oder schlecht.

Überall, wo Sie die Aufzählung als Wert speichern, fragen Sie sich möglicherweise, ob Sie sie Typestattdessen als Wert speichern können .

Um diesen Trick anzuwenden, können Sie zwei schreibgeschützte Hash-Tabellen initialisieren, nämlich:

// 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
    ...
};

Die letzte Rechtfertigung für diejenigen, die Klassen und Vererbungshierarchien vorschlagen ...

Sobald Sie sehen, dass die Aufzählungen eine Annäherung an die Vererbungshierarchie darstellen , gilt folgender Rat:

  • Entwerfen (oder verbessern) Sie zuerst Ihre Vererbungshierarchie.
  • Dann gehen Sie zurück und entwerfen Sie Ihre Aufzählungen, um diese Vererbungshierarchie zu approximieren.
rwong
quelle
Das Projekt ist eigentlich ein VBIDE-Add-In - ich analysiere und analysiere VBA-Code =)
Mathieu Guindon
1

Ich finde Ihre Verwendung von Flaggen sehr intelligent, kreativ, elegant und potenziell am effizientesten. Ich habe auch keine Probleme, es zu lesen.

Flaggen sind ein Mittel zur Signalisierung, zur Qualifizierung. Wenn ich wissen will, ob etwas Obst ist, finde ich

thingy & Organic.Fruit! = 0

lesbarer als

thingy & (Organic.Apple | Organic.Orange | Organic.Pear)! = 0

Der eigentliche Sinn von Flag-Aufzählungen besteht darin, dass Sie mehrere Status kombinieren können. Sie haben dies nur nützlicher und lesbarer gemacht. Sie vermitteln das Konzept der Frucht in Ihrem Code, ich muss nicht selbst herausfinden, dass Apfel, Orange und Birne Frucht bedeuten.

Geben Sie diesem Kerl einige Schokoladenkuchenpunkte!

Martin Maat
quelle
4
thingy is Fruitist besser lesbar als beides.
JacquesB