Est-ce mal d'utiliser des drapeaux pour «grouper» les énumérations?

12

Ma compréhension est que les [Flag]énumérations sont généralement utilisées pour des choses qui peuvent être combinées, où les valeurs individuelles ne s'excluent pas mutuellement .

Par exemple:

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

Où toute SomeAttributesvaleur peut être une combinaison de Foo, Baret Baz.

Dans un scénario plus compliqué et réel , j'utilise une énumération pour décrire 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
}

De toute évidence, une donnée Declarationne peut pas être à la fois a Variableet a LibraryProcedure- les deux valeurs individuelles ne peuvent pas être combinées ... et elles ne le sont pas.

Bien que ces indicateurs soient extrêmement utiles (il est très facile de vérifier si une donnée DeclarationTypeest un Propertyou un Module), cela semble "incorrect" car les indicateurs ne sont pas vraiment utilisés pour combiner des valeurs, mais plutôt pour les regrouper en "sous-types".

Donc, on me dit que cela abuse des drapeaux enum - cette réponse dit essentiellement que si j'ai un ensemble de valeurs applicables aux pommes et un autre ensemble applicable aux oranges, alors j'ai besoin d'un type d'énumération différent pour les pommes et un autre pour les oranges - le Le problème avec cela ici est que j'ai besoin de toutes les déclarations pour avoir une interface commune, avec DeclarationTypeêtre exposé dans la Declarationclasse de base : avoir une PropertyTypeénumération ne serait pas du tout utile.

Est-ce une conception bâclée / surprenante / abusive? Si oui, comment ce problème est-il généralement résolu?

Mathieu Guindon
la source
Considérez comment les gens vont utiliser des objets de type DeclarationType. Si je veux déterminer s'il s'agit ou non d' xun sous-type y, je vais probablement vouloir écrire cela comme x.IsSubtypeOf(y), pas comme x && y == y.
Tanner Swett
1
@TannerSwett C'est exactement ce que x.HasFlag(DeclarationType.Member)fait ....
Mathieu Guindon
Oui c'est vrai. Mais si votre méthode est appelée à la HasFlagplace de IsSubtypeOf, alors j'ai besoin d'un autre moyen de découvrir que ce que cela signifie vraiment est "est un sous-type de". Vous pouvez créer une méthode d'extension, mais en tant qu'utilisateur, ce que je trouverais le moins surprenant, c'est DeclarationTyped'être simplement une structure qui a IsSubtypeOfcomme vraie méthode.
Tanner Swett

Réponses:

10

Cela abuse définitivement des énumérations et des drapeaux! Cela pourrait fonctionner pour vous, mais quiconque lisant le code sera très confus.

Si je comprends bien, vous avez une classification hiérarchique des déclarations. C'est beaucoup trop d'informations à encoder en une seule énumération. Mais il existe une alternative évidente: utilisez les classes et l'héritage! MemberHérite donc de DeclarationType, Propertyhérite de Memberet ainsi de suite.

Les énumérations sont appropriées dans certaines circonstances particulières: si une valeur fait toujours partie d'un nombre limité d'options ou s'il s'agit d'une combinaison d'un nombre limité d'options (indicateurs). Toute information plus complexe ou structurée que celle-ci doit être représentée à l'aide d'objets.

Edit : Dans votre "scénario réel", il semble qu'il existe plusieurs endroits où le comportement est sélectionné en fonction de la valeur de l'énumération. C'est vraiment un contre-modèle, puisque vous utilisez switch+ enumcomme un "polymorphisme pauvre". Transformez simplement la valeur enum en classes distinctes encapsulant le comportement spécifique à la déclaration, et votre code sera beaucoup plus propre

JacquesB
la source
L'héritage est une bonne idée mais votre réponse implique une classe par énumération, ce qui semble exagéré.
Frank Hileman
@FrankHileman: Les "feuilles" dans la hiérarchie pourraient être représentées comme des valeurs d'énumération plutôt que des classes, mais je ne suis pas sûr que ce serait mieux. Cela dépend si un comportement différent serait associé aux différentes valeurs d'énumération, auquel cas des classes distinctes seraient mieux.
JacquesB
4
La classification n'est pas hiérarchique, les combinaisons sont des états prédéfinis. L'utilisation de l'héritage pour cela serait vraiment un abus, un abus de classes qui l'est. Avec une classe, j'attends au moins des données ou un comportement. Un tas de classes sans les deux sont ... à droite, une énumération.
Martin Maat
À propos de ce lien vers mon référentiel: le comportement par type a plus à voir avec le ParserRuleContexttype de classe généré qu'avec l'énumération. Ce code tente d'obtenir la position du jeton où insérer une As {Type}clause dans la déclaration; ces ParserRuleContextclasses dérivées sont générées par Antr4 selon une grammaire qui définit les règles de l'analyseur - Je ne contrôle pas vraiment la hiérarchie d'héritage [plutôt superficielle] des nœuds d'arbre d'analyse, bien que je puisse tirer parti de leur partial-ness pour leur faire implémenter des interfaces, par exemple pour leur faire exposer certains AsTypeClauseTokenPositionbiens .. beaucoup de travail.
Mathieu Guindon
6

Je trouve cette approche assez facile à lire et à comprendre. À mon humble avis, ce n'est pas quelque chose à confondre. Cela étant dit, cette approche me préoccupe:

  1. Ma principale réserve est qu'il n'y a aucun moyen de faire respecter cela:

    De toute évidence, une déclaration donnée ne peut pas être à la fois une variable et une procédure de bibliothèque - les deux valeurs individuelles ne peuvent pas être combinées ... et elles ne le sont pas.

    Bien que vous ne déclariez pas la combinaison ci-dessus, ce code

    var oops = DeclarationType.Variable | DeclarationType.LibraryProcedure;

    Est parfaitement valide. Et il n'y a aucun moyen de détecter ces erreurs au moment de la compilation.

  2. Il y a une limite à la quantité d'informations que vous pouvez encoder en indicateurs de bits, qui est quoi, 64 bits? Pour l'instant, vous vous rapprochez dangereusement de la taille de intet si cette énumération continue de croître, vous pourriez éventuellement manquer de bits ...

En fin de compte, je pense que c'est une approche valide, mais j'hésiterais à l'utiliser pour des hiérarchies de drapeaux grandes / complexes.

Nikita B
la source
Des tests unitaires d'analyseurs si approfondis porteraient alors sur le numéro 1. FWIW l'énumération a commencé en tant qu'énumération standard sans drapeau il y a environ 3 ans. La liste ne va pas vraiment s'allonger avec le temps non plus, mais oui, la intcapacité de battre est néanmoins une réelle préoccupation.
Mathieu Guindon
3

TL; DR Faites défiler jusqu'en bas.


D'après ce que je vois, vous implémentez un nouveau langage en plus de C #. Les énumérations semblent indiquer le type d'un identifiant (ou tout ce qui a un nom et qui apparaît dans le code source de la nouvelle langue), qui semble être appliqué aux nœuds qui doivent être ajoutés dans une arborescence du programme.

Dans cette situation particulière, il existe très peu de comportements polymorphes entre les différents types de nœuds. En d'autres termes, bien qu'il soit nécessaire que l'arbre puisse contenir des nœuds de types (variantes) très différents, la visite réelle de ces nœuds aura essentiellement recours à une chaîne géante si-alors-sinon (ou instanceof/ isvérifie). Ces contrôles géants se produiront probablement dans de nombreux endroits différents du projet. C'est la raison pour laquelle les énumérations peuvent sembler utiles, ou elles sont au moins aussi utiles que instanceof/ ischecks.

Le modèle de visiteur pourrait toujours être utile. En d'autres termes, il existe différents styles de codage qui peuvent être utilisés à la place de la chaîne géante de instanceof. Cependant, si vous voulez une discussion sur les divers avantages et inconvénients, vous auriez choisi de présenter un exemple de code de la chaîne la plus laide instanceofdu projet, au lieu de chicaner sur les énumérations.

Cela ne veut pas dire que les classes et la hiérarchie d'héritage ne sont pas utiles. Plutôt l'inverse. Bien qu'il n'y ait pas de comportements polymorphes qui fonctionnent dans tous les types de déclarations (à part le fait que chaque déclaration doit avoir une Namepropriété), il existe de nombreux comportements polymorphes riches partagés par les frères et sœurs proches. Par exemple, Functionet Procedurepartagent probablement certains comportements (les deux étant appelables et acceptant une liste d'arguments d'entrée saisis), et PropertyGethériteront certainement des comportements de Function(les deux ayant un ReturnType). Vous pouvez utiliser des énumérations ou des vérifications d'héritage pour la chaîne géante if-then-else, mais les comportements polymorphes, même fragmentés, doivent toujours être implémentés dans les classes.

Il sont beaucoup des conseils en ligne contre l' utilisation excessive de instanceof/ iscontrôles. La performance n'est pas une des raisons. La raison est plutôt d'empêcher le programmeur de découvrir organiquement des comportements polymorphes appropriés, comme si instanceof/ isest une béquille. Mais dans votre situation, vous n'avez pas d'autre choix, car ces nœuds ont très peu en commun.

Voici maintenant quelques suggestions concrètes.


Il existe plusieurs façons de représenter les groupements non foliaires.


Comparez l'extrait suivant de votre code d'origine ...

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

à cette version modifiée:

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

Cette version modifiée évite d'allouer des bits à des types de déclarations non concrets. Au lieu de cela, les types de déclaration non concrets (regroupements abstraits de types de déclaration) ont simplement des valeurs d'énumération qui sont le bit ou (l'union des bits) entre tous ses enfants.

Il y a une mise en garde: s'il existe un type de déclaration abstraite qui a un seul enfant, et s'il est nécessaire de faire la distinction entre l'abstrait (parent) et le concret (enfant), l'abstrait aura toujours besoin de son propre bit .


Une mise en garde spécifique à cette question: a Propertyest initialement un identifiant (quand vous voyez juste son nom, sans voir comment il est utilisé dans le code), mais il peut se transformer en PropertyGet/ PropertyLet/ PropertySetdès que vous voyez comment il est utilisé dans le code. En d'autres termes, à différentes étapes de l'analyse, vous devrez peut-être marquer un Propertyidentifiant comme étant "ce nom fait référence à une propriété", puis le remplacer par "cette ligne de code accède à cette propriété d'une certaine manière".

Pour résoudre cette mise en garde, vous pourriez avoir besoin de deux ensembles d'énumérations; une énumération indique ce qu'est un nom (identifiant); une autre énumération indique ce que le code essaie de faire (par exemple, déclarer le corps de quelque chose; essayer d'utiliser quelque chose d'une certaine manière).


Déterminez si les informations auxiliaires sur chaque valeur d'énumération peuvent être lues à la place à partir d'un tableau.

Cette suggestion s'exclut mutuellement avec d'autres suggestions, car elle nécessite de reconvertir les puissances de deux en petites valeurs entières non négatives.

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 maintenabilité va être problématique.


Vérifiez si un mappage un à un entre les types C # (classes dans une hiérarchie d'héritage) et vos valeurs d'énumération.

(Vous pouvez également modifier vos valeurs d'énumération pour garantir un mappage un à un avec les types.)

En C #, beaucoup de bibliothèques abusent de la Type object.GetType()méthode astucieuse , pour le meilleur ou pour le pire.

Partout où vous stockez l'énumération en tant que valeur, vous pouvez vous demander si vous pouvez le stocker en Typetant que valeur à la place.

Pour utiliser cette astuce, vous pouvez initialiser deux tables de hachage en lecture seule, à savoir:

// 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 justification finale pour ceux qui suggèrent des classes et une hiérarchie d'héritage ...

Une fois que vous pouvez voir que les énumérations sont une approximation de la hiérarchie d'héritage , les conseils suivants sont valables:

  • Concevez (ou améliorez) d'abord votre hiérarchie d'héritage,
  • Revenez ensuite en arrière et concevez vos énumérations pour approximer cette hiérarchie d'héritage.
rwong
la source
Le projet est en fait un complément VBIDE - je suis en train d'analyser et d'analyser le code VBA =)
Mathieu Guindon
1

Je trouve que votre utilisation des drapeaux est vraiment intelligente, créative, élégante et potentiellement la plus efficace. Je n'ai aucun problème à le lire non plus.

Les drapeaux sont un moyen de signaler l'état, de se qualifier. Si je veux savoir si quelque chose est un fruit, je trouve

thingy & Organic.Fruit! = 0

plus lisible que

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

Le but même des énumérations Flag est de vous permettre de combiner plusieurs états. Vous venez de rendre cela plus utile et plus lisible. Vous transmettez le concept de fruit dans votre code, je n'ai pas besoin de comprendre moi-même que pomme, orange et poire signifie fruit.

Donnez à ce gars des points de brownie!

Martin Maat
la source
4
thingy is Fruitest plus lisible que ce soit.
JacquesB