web-dev-qa-db-fra.com

Est-il erroné d'utiliser des drapeaux pour "grouper" les énumérations?

Ma compréhension est que [Flag] les énumérations sont généralement utilisées pour les 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ù n'importe quelle valeur SomeAttributes peut être une combinaison de Foo, Bar et Baz.

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

Évidemment, un Declaration donné ne peut pas être à la fois un Variable et un 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 un DeclarationType donné est un Property ou un Module), il semble "incorrect" car les indicateurs ne sont pas vraiment utilisé pour combiner les valeurs, mais plutôt pour grouper les 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 une autre pour les oranges - le problème avec cela ici est que j'ai besoin de toutes les déclarations pour avoir une interface commune, avec DeclarationType étant exposé dans la classe de base Declaration: avoir un PropertyType enum ne serait pas utile du tout.

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

12
Mathieu Guindon

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 loin pour beaucoup d'informations à encoder en une seule énumération. Mais il existe une alternative évidente: utilisez les classes et l'héritage! Donc Member hérite de DeclarationType, Property hérite de Member et 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.

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

10
JacquesB

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

  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 int et si cette énumération continue de croître, vous risquez de 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.

6
Nikita B

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-autre (ou instanceof/is chèques). 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 les vérifications instanceof/is.

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 de instanceof dans le 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 à travers chaque type de déclaration (à part le fait que chaque déclaration doit avoir une propriété Name), il existe de nombreux comportements polymorphes riches partagé par les frères et sœurs à proximité. Par exemple, Function et Procedure partagent probablement certains comportements (les deux étant appelables et acceptant une liste d'arguments d'entrée saisis), et PropertyGet héritera 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 existe de nombreux conseils en ligne contre l'utilisation excessive des contrôles instanceof/is. Les performances ne sont 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/is était 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-feuilles.


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éclaration 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 au niveau du bit ou (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 distinguer l'abstrait (parent) du concret (enfant), l'abstrait aura toujours besoin de son propre bit .


Une mise en garde spécifique à cette question: un Property est 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/PropertySet dè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 identifiant Property comme étant "ce nom fait référence à une propriété", puis le remplacer par "cette ligne de code accède à cette propriété dans une certaine façon".

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 méthode astucieuse Type object.GetType(), 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 stocker le Type en tant 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, le conseil suivant est valable:

  • 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.
3
rwong

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

L'intérêt des Flag Enums 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 Apple et orange et poire signifie fruit.

Donnez à ce gars des points de brownie!

1
Martin Maat