È sbagliato usare i flag per "raggruppare" le enumerazioni?


12

La mia comprensione è che gli [Flag]enum sono in genere utilizzati per cose che possono essere combinate, in cui i singoli valori non si escludono a vicenda .

Per esempio:

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

Dove qualsiasi SomeAttributesvalore può essere una combinazione di Foo, Bare Baz.

In uno scenario più complicato, nella vita reale , uso un enum per descrivere un 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
}

Ovviamente un dato Declarationnon può essere sia a Variableche a LibraryProcedure- i due valori individuali non possono essere combinati .. e non lo sono.

Mentre questi flag sono estremamente utili (è molto facile verificare se un dato DeclarationTypeè a Propertyo a Module), sembra "sbagliato" perché i flag non sono realmente usati per combinare valori, ma piuttosto per raggrupparli in "sottotipi".

Quindi mi è stato detto che questo sta abusando delle bandiere enum - questa risposta dice essenzialmente che se ho un set di valori applicabili alle mele e un altro set applicabile alle arance, allora ho bisogno di un diverso tipo di enum per le mele e un altro per le arance - il il problema qui è che ho bisogno che tutte le dichiarazioni abbiano un'interfaccia comune, con DeclarationTypel'esposizione nella Declarationclasse base : avere un PropertyTypeenum non sarebbe affatto utile.

È un design sciatto / sorprendente / violento? In tal caso, come viene risolto il problema?


Considera come le persone useranno oggetti di tipo DeclarationType. Se voglio determinare se è o meno xun sottotipo di y, probabilmente voglio scriverlo come x.IsSubtypeOf(y), non come x && y == y.
Tanner Swett,

1
@TannerSwett È esattamente quello che x.HasFlag(DeclarationType.Member)fa ...
Mathieu Guindon,

Sì è vero. Ma se il tuo metodo viene chiamato HasFlaginvece di IsSubtypeOf, allora ho bisogno di un altro modo per scoprire che ciò che significa veramente è "è il sottotipo di". Potresti creare un metodo di estensione, ma come utente, ciò che troverei meno sorprendente è DeclarationTypeche essere solo una struttura che ha IsSubtypeOfcome metodo reale .
Tanner Swett,

Risposte:


10

Questo sta decisamente abusando di enumerazioni e bandiere! Potrebbe funzionare per te, ma chiunque leggerà il codice sarà confuso.

Se ho capito bene, hai una classificazione gerarchica delle dichiarazioni. Questo è di gran lunga a molte informazioni per codificare in un unico enum. Ma c'è un'alternativa ovvia: usare le classi e l'eredità! Quindi Membereredita da DeclarationType, Propertyeredita da Membere così via.

Gli enumeratori sono appropriati in alcune circostanze particolari: se un valore è sempre uno di un numero limitato di opzioni o se è una combinazione di un numero limitato di opzioni (flag). Qualsiasi informazione che sia più complessa o strutturata di questa dovrebbe essere rappresentata usando oggetti.

Modifica : nel tuo "scenario di vita reale" sembra che ci siano più posti in cui il comportamento è selezionato in base al valore dell'enum. Questo è davvero un antipasto, dato che stai usando switch+ enumcome "polimorfismo dei poveri". Basta trasformare il valore enum in classi distinte che incapsulano il comportamento specifico della dichiarazione e il codice sarà molto più pulito


L'ereditarietà è una buona idea, ma la tua risposta implica una classe per enum, che sembra eccessiva.
Frank Hileman,

@FrankHileman: Le "foglie" nella gerarchia potrebbero essere rappresentate come valori enum piuttosto che come classi, ma non sono sicuro che sarebbe meglio. Dipende se comportamenti diversi sarebbero associati a valori enum diversi, nel qual caso classi distinte sarebbero migliori.
JacquesB,

4
La classificazione non è gerarchica, le combinazioni sono stati predefiniti. Usare l'eredità per questo sarebbe veramente un abuso, un abuso delle classi. Con una classe mi aspetto almeno alcuni dati o un comportamento. Un gruppo di classi senza l'una o l'altra sono ... giusto, un enum.
Martin Maat,

A proposito di quel link al mio repository: il comportamento per tipo ha più a che fare con il ParserRuleContexttipo di classe generato che con l'enum. Quel codice sta cercando di ottenere la posizione del token dove inserire una As {Type}clausola nella dichiarazione; queste ParserRuleContextclassi derivate sono generate da Antr4 in base a una grammatica che definisce le regole del parser: non controllo realmente la gerarchia di ereditarietà [piuttosto superficiale] dei nodi dell'albero di analisi, anche se posso sfruttare la loro partialgravità per farle implementare interfacce, ad esempio per farli esporre alcune AsTypeClauseTokenPositionproprietà .. un sacco di lavoro.
Mathieu Guindon,

6

Trovo questo approccio abbastanza facile da leggere e capire. IMHO, non è qualcosa di cui confondersi. Detto questo, ho alcune preoccupazioni su questo approccio:

  1. La mia prenotazione principale è che non c'è modo di far rispettare questo:

    Ovviamente una determinata Dichiarazione non può essere sia una Variable che una LibraryProcedure - i due valori individuali non possono essere combinati .. e non lo sono.

    Sebbene non dichiari la combinazione sopra, questo codice

    var oops = DeclarationType.Variable | DeclarationType.LibraryProcedure;

    È perfettamente valido E non c'è modo di rilevare quegli errori al momento della compilazione.

  2. Esiste un limite a quante informazioni è possibile codificare in flag di bit, che è 64 bit? Per ora ti stai avvicinando pericolosamente alle dimensioni di inte se questo enum continua a crescere potresti eventualmente finire i bit ...

In conclusione, penso che questo sia un approccio valido, ma esiterei a usarlo per gerarchie di bandiere grandi / complesse.


Quindi test di unità di analisi approfonditi indirizzerebbero quindi al primo posto. FWIW l'enum è iniziato come un enum standard senza bandiere circa 3 anni fa. È dopo essersi stancato di controllare sempre alcuni valori specifici (diciamo, nelle ispezioni del codice) che sono apparse le bandiere enum. La lista non crescerà davvero nel tempo, ma sì, battere la intcapacità è comunque una vera preoccupazione.
Mathieu Guindon,

3

TL; DR Scorri fino in fondo.


Da quello che vedo, stai implementando un nuovo linguaggio sopra C #. Gli enum sembrano indicare il tipo di identificatore (o qualsiasi cosa che abbia un nome e che appare nel codice sorgente della nuova lingua), che sembra essere applicato ai nodi che devono essere aggiunti in una rappresentazione ad albero del programma.

In questa particolare situazione, ci sono pochissimi comportamenti polimorfici tra i diversi tipi di nodi. In altre parole, mentre è necessario che l'albero sia in grado di contenere nodi di tipi molto diversi (varianti), l'effettiva visita di questi nodi ricorre sostanzialmente a una catena if-then-else gigante (o instanceof/ ischeck). Questi controlli giganti probabilmente avverranno in molti luoghi diversi del progetto. Questo è il motivo per cui gli enum possono sembrare utili, o sono almeno utili come instanceof/ ischecks.

Il modello visitatore potrebbe ancora essere utile. In altre parole, ci sono vari stili di codifica che possono essere usati al posto della catena gigante di instanceof. Tuttavia, se si desidera una discussione sui vari vantaggi e svantaggi, si sarebbe scelto di mostrare un esempio di codice della catena più brutta del instanceofprogetto, invece di cavarsela con enumerazioni.

Questo non significa che le classi e la gerarchia ereditaria non siano utili. Piuttosto il contrario. Sebbene non ci siano comportamenti polimorfici che agiscano su ogni tipo di dichiarazione (a parte il fatto che ogni dichiarazione deve avere una Nameproprietà), ci sono molti comportamenti polimorfici condivisi dai fratelli vicini. Ad esempio, Functione Procedureprobabilmente condividono alcuni comportamenti (essendo entrambi richiamabili e accettando un elenco di argomenti di input digitati), e PropertyGeterediteranno sicuramente comportamenti da Function(entrambi con a ReturnType). È possibile utilizzare enum o controlli di ereditarietà per la catena if-then-else gigante, ma i comportamenti polimorfici, per quanto frammentati, devono comunque essere implementati in classi.

Ci sono molti consigli online contro l'uso eccessivo di instanceof/ isassegni. Le prestazioni non sono una delle ragioni. Piuttosto, il motivo è impedire al programmatore di scoprire organicamente comportamenti polimorfici adeguati, come se instanceof/ isfosse una stampella. Ma nella tua situazione, non hai altra scelta, poiché questi nodi hanno molto poco in comune.

Ora ecco alcuni suggerimenti concreti.


Esistono diversi modi per rappresentare i raggruppamenti non foglia.


Confronta il seguente estratto del tuo codice originale ...

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

a questa versione modificata:

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

Questa versione modificata evita di allocare bit verso tipi di dichiarazione non concreti. Invece, i tipi di dichiarazione non concreti (raggruppamenti astratti di tipi di dichiarazione) hanno semplicemente valori enum che sono bitwise o (unione dei bit) in tutti i suoi figli.

C'è un avvertimento: se esiste un tipo di dichiarazione astratta che ha un singolo figlio, e se c'è bisogno di distinguere tra quello astratto (genitore) da quello concreto (figlio), allora quello astratto avrà comunque bisogno di un suo bit .


Un avvertimento specifico per questa domanda: a Propertyè inizialmente un identificatore (quando ne vedi solo il nome, senza vedere come viene utilizzato nel codice), ma può essere trasmutato in PropertyGet/ PropertyLet/ PropertySetnon appena vedi come viene utilizzato nel codice. In altre parole, nelle diverse fasi dell'analisi, potrebbe essere necessario contrassegnare un Propertyidentificatore come "questo nome si riferisce a una proprietà", e successivamente modificarlo in "questa riga di codice accede a questa proprietà in un certo modo".

Per risolvere questo avvertimento, potresti aver bisogno di due serie di enumerazioni; un enum indica cosa è un nome (identificatore); un altro enum denota ciò che il codice sta cercando di fare (ad esempio dichiarando il corpo di qualcosa; cercando di usare qualcosa in un certo modo).


Considera se invece le informazioni ausiliarie su ciascun valore enum possono essere lette da un array.

Questo suggerimento si esclude a vicenda con altri suggerimenti poiché richiede la conversione di valori di potenze di due in valori interi non negativi di piccole dimensioni.

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

La manutenibilità sarà problematica.


Verifica se un mapping uno a uno tra i tipi C # (classi in una gerarchia di ereditarietà) e i tuoi valori enum.

(In alternativa, è possibile modificare i valori di enum per garantire un mapping uno a uno con i tipi.)

In C #, molte librerie abusano del Type object.GetType()metodo elegante , nel bene e nel male.

Ovunque tu stia memorizzando l'enum come valore, potresti chiederti se puoi invece memorizzarlo Typecome valore.

Per usare questo trucco, puoi inizializzare due tabelle hash di sola lettura, vale a dire:

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

La rivendicazione finale per coloro che suggeriscono classi e gerarchia ereditaria ...

Una volta che vedi che gli enum sono un'approssimazione della gerarchia dell'ereditarietà , vale il seguente consiglio:

  • Progetta (o migliora) prima la gerarchia ereditaria,
  • Quindi torna indietro e progetta i tuoi enum per approssimare quella gerarchia ereditaria.

Il progetto è in realtà un componente aggiuntivo VBIDE - Sto analizzando e analizzando il codice VBA =)
Mathieu Guindon

1

Trovo che il tuo uso delle bandiere sia davvero intelligente, creativo, elegante e potenzialmente più efficiente. Neanche io ho problemi a leggerlo.

Le bandiere sono un mezzo per segnalare lo stato, per qualificarsi. Se voglio sapere se qualcosa è frutto, lo trovo

thingy & Organic.Fruit! = 0

più leggibile di

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

Il vero punto di enumerazione di Flag è consentire di combinare più stati. Hai appena reso questo più utile e leggibile. Trasmetti il ​​concetto di frutta nel tuo codice, non devo capire che mela, arancia e pera significano frutto.

Dai a questo ragazzo dei punti brownie!


4
thingy is Fruitè più leggibile di entrambi.
JacquesB,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.