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
/ is
cheques). 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
/ is
checks.
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 instanceof
do 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 Name
propriedade), há muitos comportamentos polimórficos ricos compartilhados por irmãos próximos. Por exemplo, Function
e Procedure
provavelmente compartilhe alguns comportamentos (sendo chamados e aceitando uma lista de argumentos de entrada digitados), e PropertyGet
definitivamente 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
/ is
cheques. 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
/ PropertySet
assim 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 Property
identificador 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 Type
como 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.
DeclarationType
. Se eu quiser determinar se é ou nãox
um subtipoy
, provavelmente vou escrever isso comox.IsSubtypeOf(y)
, não comox && y == y
.