É errado usar sinalizadores para "agrupar" enumerações?

12

Meu entendimento é que as [Flag]enums geralmente são usadas para coisas que podem ser combinadas, onde os valores individuais não são mutuamente exclusivos .

Por exemplo:

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

Onde qualquer SomeAttributesvalor pode ser uma combinação de Foo, Bare Baz.

Em um cenário da vida real mais complicado, eu uso uma enumeração para descrever 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, um dado Declarationnão pode ser a Variablee a LibraryProcedure- os dois valores individuais não podem ser combinados ... e não são.

Embora esses sinalizadores sejam extremamente úteis (é muito fácil verificar se um dado DeclarationTypeé um Propertyou a Module), parece "errado" porque os sinalizadores não são realmente usados ​​para combinar valores, mas para agrupá- los em "subtipos".

Disseram-me que isso está abusando de sinalizadores de enum - essa resposta diz essencialmente que, se eu tiver um conjunto de valores aplicáveis ​​às maçãs e outro conjunto aplicável às laranjas, preciso de um tipo de enum diferente para maçãs e outro para laranjas - o O problema com isso aqui é que eu preciso que todas as declarações tenham uma interface comum, DeclarationTypesendo expostas na Declarationclasse base : ter um PropertyTypeenum não seria útil.

É um design desleixado / surpreendente / abusivo? Se sim, então como esse problema normalmente é resolvido?

Mathieu Guindon
fonte
Considere como as pessoas vão usar objetos do tipo DeclarationType. Se eu quiser determinar se é ou não xum subtipo y, provavelmente vou escrever isso como x.IsSubtypeOf(y), não como x && y == y.
precisa
1
@TannerSwett Isso é exatamente o que x.HasFlag(DeclarationType.Member)faz ....
Mathieu Guindon
Sim, é verdade. Mas se o seu método é chamado em HasFlagvez de IsSubtypeOf, então preciso de outra maneira de descobrir que o que realmente significa é "é subtipo de". Você pode criar um método de extensão, mas como usuário, o que eu acho menos surpreendente é DeclarationTypeser apenas uma estrutura que possui IsSubtypeOfcomo método real .
precisa

Respostas:

10

Definitivamente, isso está abusando de enums e flags! Pode funcionar para você, mas qualquer pessoa que esteja lendo o código ficará muito confusa.

Se bem entendi, você tem uma classificação hierárquica de declarações. Isso está longe de muitas informações para codificar em uma única enumeração. Mas há uma alternativa óbvia: use classes e herança! Então Memberherda DeclarationType, Propertyherda Membere assim por diante.

As enums são apropriadas em algumas circunstâncias particulares: Se um valor é sempre um dentre um número limitado de opções ou se é uma combinação de um número limitado de opções (sinalizadores). Qualquer informação que seja mais complexa ou estruturada que isso deve ser representada usando objetos.

Editar : no seu "cenário da vida real", parece que há vários lugares onde o comportamento é selecionado, dependendo do valor da enumeração. Este é realmente um antipadrão, já que você está usando o switch+ enumcomo um "polimorfismo do homem pobre". Basta transformar o valor de enum em classes distintas que encapsulam o comportamento específico da declaração e seu código será muito mais limpo

JacquesB
fonte
A herança é uma boa ideia, mas sua resposta implica uma classe por enum, que parece exagerada.
precisa
@FrankHileman: As "folhas" na hierarquia podem ser representadas como valores enum e não como classes, mas não tenho certeza de que seria melhor. Depende se um comportamento diferente estiver associado aos diferentes valores de enumeração; nesse caso, classes distintas seriam melhores.
JacquesB
4
A classificação não é hierárquica, as combinações são estados predefinidos. Usar herança para isso seria realmente abuso, é abuso de classes. Com uma classe, espero pelo menos alguns dados ou algum comportamento. Um monte de classes sem as duas é ... certo, um enum.
Martin Maat
Sobre esse link para meu repositório: o comportamento por tipo tem mais a ver com o ParserRuleContexttipo de classe gerado do que com a enumeração. Esse código está tentando obter a posição do token onde inserir uma As {Type}cláusula na declaração; essas ParserRuleContextclasses derivadas são geradas pelo Antr4 de acordo com uma gramática que define as regras do analisador - eu realmente não controlo a hierarquia de herança dos nós da árvore de análise [bastante rasa], embora eu possa aproveitar sua partialcondição para implementá-las, por exemplo, fazê-los expor alguma AsTypeClauseTokenPositionpropriedade .. muito trabalho.
Mathieu Guindon
6

Acho essa abordagem fácil o suficiente para ler e entender. IMHO, não é algo para se confundir. Dito isto, tenho algumas preocupações sobre essa abordagem:

  1. Minha principal reserva é que não há como impor isso:

    Obviamente, uma dada declaração não pode ser tanto uma variável quanto um procedimento de biblioteca - os dois valores individuais não podem ser combinados ... e não são.

    Enquanto você não declara a combinação acima, esse código

    var oops = DeclarationType.Variable | DeclarationType.LibraryProcedure;

    É perfeitamente válido. E não há como detectar esses erros no momento da compilação.

  2. Há um limite para quanta informação você pode codificar em sinalizadores de bits, que são 64 bits? Por enquanto você está chegando perigosamente perto do tamanho inte, se esse enum continuar crescendo, você poderá acabar ficando sem bits ...

Resumindo, acho que essa é uma abordagem válida, mas hesitaria em usá-la para hierarquias grandes / complexas de sinalizadores.

Nikita B
fonte
Portanto , testes detalhados de unidade do analisador abordariam o número 1. FWIW, o enum começou como um enum sem sinalizadores padrão há cerca de 3 anos. Depois de se cansar de sempre verificar se há alguns valores específicos (por exemplo, nas inspeções de código), os sinalizadores de enumeração apareceram. A lista também não vai crescer com o tempo, mas sim, superar a intcapacidade é uma preocupação real.
Mathieu Guindon
3

TL; DR Role até a parte inferior.


Pelo que vejo, você está implementando uma nova linguagem em cima de C #. As enumerações parecem indicar o tipo de um identificador (ou qualquer coisa que tenha um nome e apareça no código-fonte do novo idioma), que parece ser aplicada a nós que devem ser adicionados a uma representação em árvore do programa.

Nessa situação específica, existem muito poucos comportamentos polimórficos entre os diferentes tipos de nós. Em outras palavras, embora seja necessário que a árvore seja capaz de conter nós de tipos muito diferentes (variantes), a visitação real desses nós basicamente recorrerá a uma cadeia gigantesca de if-then-else (ou instanceof/ ischeques). Essas verificações gigantescas provavelmente acontecerão em muitos lugares diferentes do projeto. Essa é a razão pela qual as enums podem parecer úteis ou, pelo menos, são tão úteis quanto instanceof/ ischecks.

O padrão do visitante ainda pode ser útil. Em outras palavras, existem vários estilos de codificação que podem ser usados ​​no lugar da cadeia gigante de instanceof. No entanto, se você quiser uma discussão sobre os vários benefícios e desvantagens, você escolheria mostrar um exemplo de código da cadeia mais feia instanceofdo projeto, em vez de discutir sobre enumerações.

Isso não quer dizer que classes e hierarquia de herança não sejam úteis. Muito pelo contrário. Embora não haja comportamentos polimórficos que funcionem em todos os tipos de declaração (além do fato de que toda declaração deve ter uma Namepropriedade), há muitos comportamentos polimórficos ricos compartilhados por irmãos próximos. Por exemplo, Functione Procedureprovavelmente compartilhe alguns comportamentos (sendo chamados e aceitando uma lista de argumentos de entrada digitados), e PropertyGetdefinitivamente herdará comportamentos de Function(ambos com um ReturnType). Você pode usar enumerações ou verificações de herança para a cadeia gigante if-then-else, mas os comportamentos polimórficos, mesmo fragmentados, ainda devem ser implementados nas classes.

Existem muitos conselhos on-line contra uso excessivo de instanceof/ ischeques. O desempenho não é um dos motivos. Em vez disso, a razão é evitar que o programador de organicamente descoberta de comportamentos polimórficos adequados, como se instanceof/ isé uma muleta. Mas na sua situação, você não tem outra escolha, pois esses nós têm muito pouco em comum.

Agora, aqui estão algumas sugestões concretas.


Existem várias maneiras de representar os agrupamentos não-folha.


Compare o seguinte trecho do seu 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,
}

para esta versão 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 versão modificada evita a alocação de bits para tipos de declaração não concretos. Em vez disso, tipos de declaração não concretos (agrupamentos abstratos de tipos de declaração) simplesmente têm valores de enumeração que são bit a bit ou (união dos bits) em todos os seus filhos.

Há uma ressalva: se houver um tipo de declaração abstrata que tenha um único filho e se for necessário distinguir entre o abstrato (pai) do concreto (filho), o abstrato ainda precisará de seu próprio bit .


Uma ressalva específica a essa pergunta: a Propertyé inicialmente um identificador (quando você apenas vê seu nome, sem ver como é usado no código), mas pode transmutar para PropertyGet/ PropertyLet/ PropertySetassim que você vê como está sendo usado. no código Em outras palavras, em diferentes estágios da análise, você pode precisar marcar um Propertyidentificador como "esse nome se refere a uma propriedade" e depois alterar para "essa linha de código está acessando essa propriedade de uma certa maneira".

Para resolver essa ressalva, você pode precisar de dois conjuntos de enumerações; uma enum indica o que é um nome (identificador); outra enumeração indica o que o código está tentando fazer (por exemplo, declarar o corpo de algo; tentar usar algo de uma certa maneira).


Considere se as informações auxiliares sobre cada valor de enum podem ser lidas em uma matriz.

Essa sugestão é mutuamente exclusiva com outras sugestões, pois requer a conversão de valores de potências de dois para valores inteiros não negativos pequenos.

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

A manutenção será problemática.


Verifique se um mapeamento individual entre tipos C # (classes em uma hierarquia de herança) e seus valores de enumeração.

(Como alternativa, você pode ajustar seus valores de enumeração para garantir um mapeamento individual com tipos.)

Em C #, muitas bibliotecas abusam do Type object.GetType()método bacana , para o bem ou para o mal.

Em qualquer lugar que você esteja armazenando a enumeração como um valor, você pode se perguntar se pode armazená-la Typecomo um valor.

Para usar esse truque, você pode inicializar duas tabelas de hash somente leitura, 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
    ...
};

A vindicação final para aqueles que sugerem classes e hierarquia de herança ...

Depois de ver que as enumerações são uma aproximação à hierarquia de herança , o seguinte conselho é válido:

  • Projete (ou melhore) sua hierarquia de herança primeiro,
  • Em seguida, volte e crie suas enumerações para aproximar essa hierarquia de herança.
rwong
fonte
O projeto é uma VBIDE add-in na verdade - eu sou analisar e análise de código VBA =)
Mathieu Guindon
1

Acho que o uso de sinalizadores é realmente inteligente, criativo, elegante e potencialmente mais eficiente. Também não tenho problemas em lê-lo.

Os sinalizadores são um meio de sinalizar o estado, de se qualificar. Se eu quero saber se algo é fruto, acho

coisinha & Organic.Fruit! = 0

mais legível que

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

O ponto principal das enumerações de Flag é permitir que você combine vários estados. Você acabou de tornar isso mais útil e legível. Você transmite o conceito de fruta em seu código, não preciso me entender que maçã, laranja e pêra significam frutas.

Dê a esse cara alguns pontos de brownie!

Martin Maat
fonte
4
thingy is Fruité mais legível do que qualquer um.
precisa saber é o seguinte