¿Es incorrecto usar banderas para enumerar "grupos"?

12

Tengo entendido que las [Flag]enumeraciones se usan generalmente para cosas que se pueden combinar, donde los valores individuales no son mutuamente excluyentes .

Por ejemplo:

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

Donde cualquier SomeAttributesvalor puede ser una combinación de Foo, Bary Baz.

En un escenario de la vida real más complicado, uso una enumeración para describir 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
}

Obviamente, un dado Declarationno puede ser tanto a Variablecomo a LibraryProcedure- los dos valores individuales no pueden combinarse ... y no lo son.

Si bien estos indicadores son extremadamente útiles (es muy fácil verificar si un dado DeclarationTypees a Propertyo a Module), se siente "mal" porque los indicadores no se usan realmente para combinar valores, sino para agruparlos en "subtipos".

Así que me dicen que esto está abusando de las banderas de enumeración: esta respuesta esencialmente dice que si tengo un conjunto de valores aplicables a las manzanas y otro conjunto aplicable a las naranjas, entonces necesito un tipo de enumeración diferente para las manzanas y otro para las naranjas: el El problema con esto aquí es que necesito que todas las declaraciones tengan una interfaz común, con DeclarationTypeestar expuesto en la Declarationclase base : tener una PropertyTypeenumeración no sería útil en absoluto.

¿Es este un diseño descuidado / sorprendente / abusivo? Si es así, ¿cómo se resuelve ese problema típicamente?

Mathieu Guindon
fuente
Considere cómo las personas van a usar objetos de tipo DeclarationType. Si quiero determinar si es o no xun subtipo de y, probablemente quiera escribir eso como x.IsSubtypeOf(y), no como x && y == y.
Tanner Swett
1
@TannerSwett Eso es exactamente lo que x.HasFlag(DeclarationType.Member)hace ...
Mathieu Guindon
Sí, eso es verdad. Pero si se llama a su método en HasFlaglugar de IsSubtypeOf, entonces necesito otra forma de descubrir que lo que realmente significa es "es un subtipo de". Podrías crear un método de extensión, pero como usuario, lo que me parece menos sorprendente es DeclarationTypeser una estructura que tiene IsSubtypeOfcomo método real .
Tanner Swett

Respuestas:

10

¡Esto definitivamente está abusando de enumeraciones y banderas! Puede funcionar para usted, pero cualquier otra persona que lea el código estará muy confundido.

Si entiendo correctamente, tiene una clasificación jerárquica de declaraciones. Este es el momento de mucha información para codificar en una sola enumeración. Pero hay una alternativa obvia: ¡usar clases y herencia! Entonces Memberhereda de DeclarationType, Propertyhereda de Membery así sucesivamente.

Las enumeraciones son apropiadas en algunas circunstancias particulares: si un valor siempre es una de un número limitado de opciones, o si es una combinación de un número limitado de opciones (banderas). Cualquier información que sea más compleja o estructurada que esta debería representarse utilizando objetos.

Editar : en su "escenario de la vida real" parece que hay varios lugares donde se selecciona el comportamiento en función del valor de la enumeración. Esto es realmente un antipatrón, ya que está utilizando switch+ enumcomo un "polimorfismo del hombre pobre". Simplemente convierta el valor enum en clases distintas que encapsulan el comportamiento específico de la declaración, y su código será mucho más limpio

JacquesB
fuente
La herencia es una buena idea, pero su respuesta implica una clase por enumeración, lo que parece excesivo.
Frank Hileman
@FrankHileman: Las "hojas" en la jerarquía podrían representarse como valores de enumeración en lugar de clases, pero no estoy seguro de que sería mejor. Depende de si se asociaría un comportamiento diferente con los diferentes valores de enumeración, en cuyo caso las clases distintas serían mejores.
JacquesB
44
La clasificación no es jerárquica, las combinaciones son estados predefinidos. Usar la herencia para esto sería realmente abuso, abuso de clases. Con una clase espero al menos algunos datos o algún comportamiento. Un montón de clases sin ninguno de los dos ... correcto, una enumeración.
Martin Maat
Acerca de ese enlace a mi repositorio: el comportamiento por tipo tiene más que ver con el ParserRuleContexttipo de clase generada que con la enumeración. Ese código está tratando de obtener la posición del token donde insertar una As {Type}cláusula en la declaración; ParserRuleContextAntr4 genera estas clases derivadas según una gramática que define las reglas del analizador: realmente no controlo la jerarquía de herencia [bastante superficial] de los nodos del árbol de análisis, aunque puedo aprovechar su capacidad partialpara hacer que implementen interfaces, por ejemplo, para hacer que expongan alguna AsTypeClauseTokenPositionpropiedad ... mucho trabajo.
Mathieu Guindon
6

Este enfoque me resulta bastante fácil de leer y comprender. En mi humilde opinión, no es algo de lo que confundirse. Dicho esto, tengo algunas preocupaciones sobre este enfoque:

  1. Mi principal reserva es que no hay forma de hacer cumplir esto:

    Obviamente, una declaración dada no puede ser tanto un procedimiento de variable como de biblioteca: los dos valores individuales no se pueden combinar ... y no lo son.

    Si bien no declaras la combinación anterior, este código

    var oops = DeclarationType.Variable | DeclarationType.LibraryProcedure;

    Es perfectamente valido. Y no hay forma de detectar esos errores en el momento de la compilación.

  2. Hay un límite para la cantidad de información que puede codificar en banderas de bits, que es qué, ¿64 bits? Por ahora te estás acercando peligrosamente al tamaño de inty si esta enumeración sigue creciendo, es posible que eventualmente se quede sin pedazos ...

En pocas palabras, creo que este es un enfoque válido, pero dudaría en usarlo para jerarquías de banderas grandes / complejas.

Nikita B
fuente
Entonces , las pruebas exhaustivas de la unidad del analizador abordarían el n. ° 1. FWIW la enumeración comenzó como una enumeración estándar sin banderas hace unos 3 años. Después de cansarse de comprobar siempre algunos valores específicos (por ejemplo, en las inspecciones de código) aparecieron las banderas de enumeración. La lista tampoco va a crecer con el tiempo, pero sí la intcapacidad de ganar es una preocupación real.
Mathieu Guindon
3

TL; DR Desplácese hasta el final.


Por lo que veo, estás implementando un nuevo lenguaje además de C #. Las enumeraciones parecen denotar el tipo de un identificador (o cualquier cosa que tenga un nombre y que aparezca en el código fuente del nuevo idioma), que parece aplicarse a los nodos que se agregarán a una representación de árbol del programa.

En esta situación particular, hay muy pocos comportamientos polimórficos entre los diferentes tipos de nodos. En otras palabras, si bien es necesario que el árbol pueda contener nodos de tipos muy diferentes (variantes), la visita real de estos nodos básicamente recurrirá a una cadena gigante de si-entonces-otro (o instanceof/ iscontroles). Es probable que estos controles gigantes sucedan en muchos lugares diferentes en todo el proyecto. Esta es la razón por la cual las enumeraciones pueden parecer útiles, o son al menos tan útiles como instanceof/ ischeques.

El patrón de visitante aún puede ser útil. En otras palabras, hay varios estilos de codificación que se pueden usar en lugar de la cadena gigante de instanceof. Sin embargo, si desea una discusión sobre los diversos beneficios y desventajas, habría elegido mostrar un ejemplo de código de la cadena más fea instanceofdel proyecto, en lugar de discutir sobre enumeraciones.

Esto no quiere decir que las clases y la jerarquía de herencia no sean útiles. Todo lo contrario. Si bien no hay comportamientos polimórficos que funcionen en todos los tipos de declaración (aparte del hecho de que cada declaración debe tener una Namepropiedad), hay muchos comportamientos polimórficos ricos compartidos por hermanos cercanos. Por ejemplo, Functiony Procedureprobablemente comparta algunos comportamientos (ambos sean invocables y acepten una lista de argumentos de entrada escritos), y PropertyGetdefinitivamente heredarán comportamientos de Function(ambos tienen un ReturnType). Puede usar enumeraciones o verificaciones de herencia para la cadena gigante if-then-else, pero los comportamientos polimórficos, aunque fragmentados, aún deben implementarse en clases.

Hay muchos consejos en línea contra el uso excesivo de instanceof/ iscontroles. El rendimiento no es una de las razones. Por el contrario, la razón es evitar que el programador de descubrir orgánicamente comportamientos polimórficos apropiados, como si instanceof/ ises una muleta. Pero en su situación, no tiene otra opción, ya que estos nodos tienen muy poco en común.

Ahora aquí hay algunas sugerencias concretas.


Hay varias formas de representar las agrupaciones que no son hojas.


Compare el siguiente extracto de su código original ...

[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 esta versión modificada:

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

Esta versión modificada evita la asignación de bits hacia tipos de declaración no concretos. En cambio, los tipos de declaración no concretos (agrupaciones abstractas de tipos de declaración) simplemente tienen valores de enumeración que son bit a bit (o unión de bits) en todos sus elementos secundarios.

Hay una advertencia: si hay un tipo de declaración abstracta que tiene un solo hijo, y si es necesario distinguir entre el abstracto (padre) del concreto (hijo), entonces el abstracto aún necesitará su propio bit .


Una advertencia que es específica para esta pregunta: a Propertyes inicialmente un identificador (cuando solo ve su nombre, sin ver cómo se usa en el código), pero puede transmutarse en PropertyGet/ PropertyLet/ PropertySettan pronto como vea cómo se está usando. en el código En otras palabras, en diferentes etapas del análisis, es posible que deba marcar un Propertyidentificador como "este nombre se refiere a una propiedad", y luego cambiarlo a "esta línea de código está accediendo a esta propiedad de cierta manera".

Para resolver esta advertencia, es posible que necesite dos conjuntos de enumeraciones; una enumeración denota lo que es un nombre (identificador); otra enumeración denota lo que el código está tratando de hacer (por ejemplo, declarando el cuerpo de algo; tratando de usar algo de cierta manera).


Considere si la información auxiliar sobre cada valor de enumeración puede leerse desde una matriz en su lugar.

Esta sugerencia es mutuamente exclusiva con otras sugerencias porque requiere convertir los valores de potencias de dos a valores enteros pequeños no negativos.

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 mantenibilidad será problemática.


Compruebe si una asignación uno a uno entre los tipos de C # (clases en una jerarquía de herencia) y sus valores de enumeración.

(Alternativamente, puede ajustar sus valores de enumeración para garantizar una asignación uno a uno con los tipos).

En C #, muchas bibliotecas abusan del Type object.GetType()método ingenioso , para bien o para mal.

En cualquier lugar donde esté almacenando la enumeración como valor, puede preguntarse si puede almacenarla Typecomo valor.

Para usar este truco, puede inicializar dos tablas hash de solo lectura, a saber:

// 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 reivindicación final para aquellos que sugieren clases y jerarquía de herencia ...

Una vez que puede ver que las enumeraciones son una aproximación a la jerarquía de herencia , el siguiente consejo es válido:

  • Diseñe (o mejore) su jerarquía de herencia primero,
  • Luego regrese y diseñe sus enumeraciones para aproximar esa jerarquía de herencia.
rwong
fuente
El proyecto es un complemento VBIDE en realidad: estoy analizando y analizando el código VBA =)
Mathieu Guindon
1

Creo que su uso de banderas es realmente inteligente, creativo, elegante y potencialmente más eficiente. No tengo ningún problema para leerlo tampoco.

Las banderas son un medio de señalizar el estado, de calificar. Si quiero saber si algo es fruta, encuentro

cosita y orgánica ¡Fruta! = 0

más legible que

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

El objetivo de las enumeraciones de Flag es permitirle combinar varios estados. Acabas de hacer esto más útil y legible. Transmite el concepto de fruta en su código, no tengo que entender que manzana, naranja y pera significan fruta.

¡Dale a este chico algunos puntos brownie!

Martin Maat
fuente
44
thingy is Fruites más legible que cualquiera de los dos.
JacquesB