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
/ is
check). 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
/ is
checks.
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 instanceof
progetto, 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 Name
proprietà), ci sono molti comportamenti polimorfici condivisi dai fratelli vicini. Ad esempio, Function
e Procedure
probabilmente condividono alcuni comportamenti (essendo entrambi richiamabili e accettando un elenco di argomenti di input digitati), e PropertyGet
erediteranno 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
/ is
assegni. Le prestazioni non sono una delle ragioni. Piuttosto, il motivo è impedire al programmatore di scoprire organicamente comportamenti polimorfici adeguati, come se instanceof
/ is
fosse 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
/ PropertySet
non appena vedi come viene utilizzato nel codice. In altre parole, nelle diverse fasi dell'analisi, potrebbe essere necessario contrassegnare un Property
identificatore 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 Type
come 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.
DeclarationType
. Se voglio determinare se è o menox
un sottotipo diy
, probabilmente voglio scriverlo comex.IsSubtypeOf(y)
, non comex && y == y
.