Не правильно ли использовать флаги для «группировки» перечислений?

12

Насколько я понимаю, [Flag]перечисления обычно используются для вещей, которые можно комбинировать, когда отдельные значения не являются взаимоисключающими .

Например:

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

Если какое - либо SomeAttributesзначение может быть комбинацией Foo, Barи Baz.

В более сложном, реальном сценарии я использую перечисление для описания 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
}

Очевидно, что данное Declarationне может быть и a, Variableи a LibraryProcedure- два отдельных значения не могут быть объединены .. и это не так.

Хотя эти флаги чрезвычайно полезны (очень легко проверить, является ли данный DeclarationTypeтип a Propertyили a Module), он кажется «неправильным», поскольку флаги на самом деле используются не для объединения значений, а для группировки их в «подтипы».

Поэтому мне сказали, что это неправильно использует флаги enum - этот ответ по существу говорит, что если у меня есть набор значений, применимый к яблокам, и другой набор, применимый к апельсинам, то мне нужен другой тип enum для яблок и другой тип для апельсинов - Проблема здесь в том, что мне нужно, чтобы все объявления имели общий интерфейс, и DeclarationTypeчтобы они были представлены в базовом Declarationклассе: наличие PropertyTypeenum не было бы полезным вообще.

Это небрежный / удивительный / оскорбительный дизайн? Если да, то как обычно решается эта проблема?

Матье Гиндон
источник
Подумайте, как люди собираются использовать объекты типа DeclarationType. Если я хочу определить, xявляется ли это подтипом или нет y, я, вероятно, захочу написать это как x.IsSubtypeOf(y), а не как x && y == y.
Таннер Светт
1
@TannerSwett Это именно то, что x.HasFlag(DeclarationType.Member)делает ....
Матье
Да, это правда. Но если ваш метод вызывается HasFlagвместо IsSubtypeOf, тогда мне нужен какой-то другой способ выяснить, что он на самом деле означает «это подтип». Можно создать метод расширения, но как пользователь, что я бы найти хотя бы удивительно для DeclarationTypeпросто быть структура , которая имеет в IsSubtypeOfкачестве реального способа.
Таннер Светт

Ответы:

10

Это определенно злоупотребляет перечислениями и флагами! Это может сработать для вас, но любой, кто читает код, будет сильно сбит с толку.

Если я правильно понимаю, у вас есть иерархическая классификация объявлений. Это далеко к большому количеству информации , чтобы закодировать в одном перечислении. Но есть очевидная альтернатива: используйте классы и наследование! Так Memberнаследует от DeclarationType, Propertyнаследует от Memberи так далее.

Перечисления подходят для некоторых конкретных обстоятельств: если значение всегда является одним из ограниченного числа параметров, или если это любая комбинация ограниченного числа параметров (флагов). Любая информация, которая является более сложной или структурированной, чем эта, должна быть представлена ​​с использованием объектов.

Изменить : в вашем "реальном сценарии" кажется, что есть несколько мест, где поведение выбирается в зависимости от значения перечисления. Это действительно антипаттерн, так как вы используете switch+ enumкак «полиморфизм бедняков». Просто превратите значение enum в отдельные классы, инкапсулирующие поведение, специфичное для объявления, и ваш код станет намного чище

JacquesB
источник
Наследование - хорошая идея, но ваш ответ подразумевает класс на перечисление, что кажется излишним.
Фрэнк Хилман
@FrankHileman: «Листья» в иерархии могут быть представлены в виде значений enum, а не классов, но я не уверен, что это будет лучше. Зависит от того, будет ли связано другое поведение с различными значениями перечисления, и в этом случае отдельные классы будут лучше.
JacquesB
4
Классификация не является иерархической, комбинации представляют собой заранее определенные состояния. Использование наследования для этого было бы действительно злоупотреблением, злоупотреблением классами, которое есть. С классом я ожидаю, по крайней мере, некоторые данные или поведение. Группа классов без любого из них ... правильно, перечисление.
Мартин Маат
Об этой ссылке на мое хранилище: поведение каждого типа больше связано с ParserRuleContextтипом сгенерированного класса, чем с enum. Этот код пытается получить позицию токена, куда вставить As {Type}предложение в объявлении; эти ParserRuleContextклассы-получатели генерируются Antr4 в соответствии с грамматикой, которая определяет правила синтаксического анализа - я на самом деле не контролирую [довольно поверхностную] иерархию наследования узлов синтаксического дерева, хотя я могу использовать их возможности partialдля реализации интерфейсов, например, для заставить их выставить какую-то AsTypeClauseTokenPositionсобственность .. много работы.
Матье
6

Я считаю этот подход достаточно легким для чтения и понимания. ИМХО, это не то, чтобы запутаться. При этом у меня есть некоторые опасения по поводу этого подхода:

  1. Моя главная оговорка в том, что нет способа обеспечить это:

    Очевидно, что данная Декларация не может быть одновременно Переменной и Библиотечной процедурой - два отдельных значения не могут быть объединены ... и это не так.

    Пока вы не объявляете вышеуказанную комбинацию, этот код

    var oops = DeclarationType.Variable | DeclarationType.LibraryProcedure;

    Совершенно верно. И нет способа отловить эти ошибки во время компиляции.

  2. Существует ограничение на количество информации, которую вы можете кодировать в битовых флагах, а что, 64 бита? На данный момент вы приближаетесь к размеру опасно, intи если этот список продолжает расти, вы можете в конечном итоге просто исчерпать себя ...

Суть в том, что я думаю, что это правильный подход, но я бы не решился использовать его для больших / сложных иерархий флагов.

Никита Б
источник
Таким образом, тщательные юнит-тесты парсера будут адресованы №1. FWIW перечисление началось как стандартное перечисление без флагов около 3 лет назад. После того, как мы устали всегда проверять некоторые конкретные значения (скажем, в проверках кода), появились флаги перечисления. Список на самом деле тоже не будет расти со временем, но, intтем не менее, поражение мощностей вызывает серьезную обеспокоенность.
Матье
3

TL; DR Прокрутите до самого низа.


Из того, что я вижу, вы внедряете новый язык поверх C #. Перечисления, по-видимому, обозначают тип идентификатора (или всего, что имеет имя и появляется в исходном коде нового языка), которое, кажется, применяется к узлам, которые должны быть добавлены в древовидное представление программы.

В этой конкретной ситуации очень мало полиморфных поведений между различными типами узлов. Другими словами, в то время как это необходимо для дерева , чтобы иметь возможность содержать узлы самых разных типов (варианты), фактическое посещение этих узлов будет в основном прибегают к гигантскому , если-то-иначе цепь (или instanceof/ isпроверки). Эти гигантские проверки, скорее всего, произойдут во многих местах проекта. По этой причине перечисления могут показаться полезными или, по крайней мере, такими же полезными, как instanceof/ isпроверки.

Шаблон посетителя может все еще быть полезным. Другими словами, существуют различные стили кодирования, которые можно использовать вместо гигантской цепочки instanceof. Однако, если вы хотите обсудить различные преимущества и недостатки, вы бы предпочли продемонстрировать пример кода из самой уродливой цепочки instanceofпроекта, а не спорить о перечислениях.

Это не значит, что классы и иерархия наследования бесполезны. Наоборот. Хотя не существует полиморфных поведений, которые бы работали с каждым типом объявления (кроме того факта, что у каждого объявления должно быть Nameсвойство), существует множество богатых полиморфных поведений, общих для соседних братьев и сестер. Например, Functionи, Procedureвероятно, разделяют некоторые поведения (оба являются вызываемыми и принимают список типизированных входных аргументов), и PropertyGetопределенно будут наследовать поведение от Function(оба имеют a ReturnType). Вы можете использовать перечисления или проверки наследования для гигантской цепочки if-then-else, но полиморфное поведение, пусть и фрагментированное, все же должно быть реализовано в классах.

Есть много онлайн-советов против чрезмерного использования instanceof/ isпроверок. Производительность не является одной из причин. Скорее, причина в том, чтобы не дать программисту органически обнаружить подходящие полиморфные поведения, как будто instanceof/ isэто опора. Но в вашей ситуации у вас нет другого выбора, так как у этих узлов очень мало общего.

Теперь вот несколько конкретных предложений.


Есть несколько способов представления неконечных групп.


Сравните следующую выдержку из вашего исходного кода ...

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

к этой модифицированной версии:

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

Эта измененная версия избегает выделения битов для неконкретных типов объявлений. Вместо этого, неконкретные типы объявлений (абстрактные группы типов объявлений) просто имеют значения перечисления, которые являются побитовым (или объединением битов) для всех его дочерних элементов.

Существует предостережение: если существует абстрактный тип объявления, у которого есть один дочерний элемент, и если необходимо различать абстрактный (родительский) от конкретного (дочернего), то абстрактному по-прежнему потребуется свой собственный бит ,


Одно предостережение, характерное для этого вопроса: a Propertyизначально является идентификатором (когда вы просто видите его имя, не видя, как оно используется в коде), но оно может преобразоваться в PropertyGet/ PropertyLet/, PropertySetкак только вы увидите, как оно используется в коде. Другими словами, на разных этапах анализа вам может понадобиться пометить Propertyидентификатор как «это имя относится к свойству», а затем изменить его на «эта строка кода обращается к этому свойству определенным образом».

Чтобы решить эту проблему, вам может понадобиться два набора перечислений; одно перечисление обозначает, что такое имя (идентификатор); другое перечисление обозначает, что код пытается сделать (например, объявлять тело чего-либо; пытаться использовать что-то определенным образом).


Подумайте, можно ли вместо этого считывать из массива вспомогательную информацию о каждом значении перечисления.

Это предложение является взаимоисключающим с другими предложениями, потому что оно требует преобразования значений степеней двух обратно в маленькие неотрицательные целые значения.

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

Ремонтопригодность будет проблематичной.


Проверьте, соответствует ли взаимно-однозначное соответствие между типами C # (классы в иерархии наследования) и значениями перечисления.

(В качестве альтернативы вы можете настроить значения перечисления, чтобы обеспечить однозначное сопоставление с типами.)

В C # многие библиотеки злоупотребляют изящным Type object.GetType()методом, хорошим или плохим.

Везде, где вы храните enum как значение, вы можете спросить себя, можете ли вы сохранить Typeвместо него значение.

Чтобы использовать этот трюк, вы можете инициализировать две хеш-таблицы только для чтения, а именно:

// 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
    ...
};

Последнее оправдание для тех, кто предлагает классы и иерархию наследования ...

Как только вы увидите, что перечисления являются приближением к иерархии наследования , действует следующий совет:

  • Сначала спроектируйте (или улучшите) иерархию наследования,
  • Затем вернитесь и спроектируйте свои перечисления, чтобы приблизить эту иерархию наследования.
rwong
источник
Проект на самом деле является надстройкой VBIDE - я анализирую и анализирую код VBA =)
Матье
1

Я считаю, что вы используете флаги действительно умно, креативно, элегантно и потенциально наиболее эффективно. У меня тоже нет проблем с чтением.

Флаги являются средством сигнализации состояния, уточнения. Если я хочу знать, если что-то фрукт, я нахожу

штука и органика. Фрукт! = 0

более читабельным, чем

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

Суть перечисления флагов заключается в том, чтобы позволить вам объединять несколько состояний. Вы просто сделали это более полезным и читабельным. Вы передаете концепцию фруктов в своем коде, мне не нужно понимать, что яблоко, апельсин и груша означают фрукты.

Дайте этому парню несколько очков брауни!

Мартин Маат
источник
4
thingy is Fruitболее читабельным, чем любой, хотя.
JacquesB