Чи неправильно використовувати прапори для "групування" переліків?


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є Propertyабо Module), він відчуває себе «неправильно» , тому що прапори не реально використовується для об'єднання значень, а для угруповання їх у «підвиди».

Тому мені кажуть, що це зловживання прапорами перерахунків - ця відповідь по суті говорить про те, що якщо у мене є набір значень, застосовних до яблук, та інший набір, що застосовується до апельсинів, то мені потрібен інший тип перерахунку для яблук та інший для апельсинів - Проблема в цьому полягає в тому, що мені потрібно, щоб усі декларації мали загальний інтерфейс, щоб DeclarationTypeвони опинилися в базовому Declarationкласі: мати PropertyTypeперерахунок взагалі не було б корисно.

Це неохайний / дивовижний / образливий дизайн? Якщо так, то як вирішується ця проблема?


Поміркуйте, як люди збираються використовувати об’єкти типу DeclarationType. Якщо я хочу визначити, є чи ні xпідтипом y, я, мабуть, хочу записати це як x.IsSubtypeOf(y), а не як x && y == y.
Tanner Swett

1
@TannerSwett Це саме те, що x.HasFlag(DeclarationType.Member)робить ....
Mathieu Guindon

Так, це правда. Але якщо ваш метод називається HasFlagзамість IsSubtypeOf, тоді мені потрібен інший спосіб з'ясувати, що те, що воно насправді означає, є "підтипом". Можна створити метод розширення, але як користувач, що я б знайти хоча б дивно для DeclarationTypeпросто бути структура , яка має в IsSubtypeOfякості реального способу.
Tanner Swett

Відповіді:


10

Це, безумовно, зловживання переліками та прапорами! Це може працювати для вас, але хто-небудь ще читає код буде сильно заплутаний.

Якщо я правильно розумію, у вас є ієрархічна класифікація декларацій. Це далеко не багато інформації для кодування в одному перерахунку. Але є очевидна альтернатива: Використовуйте класи та успадкування! Так Memberуспадковує від DeclarationType, Propertyуспадковує від Memberтощо.

Переліки є доцільними в деяких конкретних обставинах: Якщо значення завжди є одним з обмеженої кількості варіантів, або якщо це будь-яке поєднання обмеженої кількості опцій (прапорів). Будь-яка інформація, яка є більш складною або структурованою, ніж ця, повинна бути представлена ​​за допомогою об'єктів.

Редагувати : У вашому "сценарії реального життя", здається, є кілька місць, де поведінка вибирається залежно від значення перерахунку. Це справді антипатерн, оскільки ви використовуєте switch+ enumяк "поліморфізм бідних людей". Просто перетворіть значення enum у окремі класи, що капсулюють поведінку, пов’язану з декларацією, і ваш код стане набагато чистішим


Спадщина - це гарна ідея, але ваша відповідь передбачає клас за перерахунком, який здається непосильним.
Френк Хілеман

@FrankHileman: "Листи" в ієрархії можна представити як значення перерахунку, а не класи, але я не впевнений, що це було б краще. Залежить, чи буде пов'язана різна поведінка з різними значеннями перерахунків, і в цьому випадку окремі класи будуть кращими.
ЖакБ

4
Класифікація не є ієрархічною, комбінації - це попередньо визначені стани. Використовувати спадщину для цього було б справді зловживанням, зловживанням класами. З класом я очікую хоча б деяких даних чи певної поведінки. Купа занять без жодного з них - так, перерахунок.
Мартін Мейт

Про це посилання на моє репо: поведінка кожного типу пов'язана більше з ParserRuleContextтипом згенерованого класу, ніж із перерахунком. Цей код намагається отримати маркерну позицію, куди слід вставити As {Type}пункт у декларацію; ці ParserRuleContextпохідні класи генеруються Antr4 за граматику, яка визначає правила аналізатора - я не дуже керую ієрархією спадкових дерев розбору (досить мілкою), хоча я можу використовувати їх partial-ness, щоб змусити їх реалізувати інтерфейси, наприклад, для змусити їх виставити якусь AsTypeClauseTokenPositionвласність .. багато роботи.
Матьє Гіндон

6

Я вважаю такий підхід досить простим для читання та розуміння. ІМХО, це не те, що плутати. Попри це, у мене є деякі занепокоєння щодо цього підходу:

  1. Головне моє застереження - це неможливо виконати це:

    Очевидно, що дана декларація не може бути як змінною, так і бібліотечною процедурою - два окремих значення не можна поєднувати .. і вони не є.

    Хоча ви не заявляєте вище комбінацію, цей код

    var oops = DeclarationType.Variable | DeclarationType.LibraryProcedure;

    Цілком справедливий. І немає ніякого способу зловити ці помилки під час компіляції.

  2. Існує обмеження на кількість інформації, яку ви можете закодувати в бітові прапори, а це що, 64 біти? Наразі ви наближаєтесь до розміру небезпечно, intі якщо цей перелік зростатиме, з часом ви можете просто закінчитись ...

Підсумок є, я вважаю, що це правильний підхід, але я б вагався використовувати його для великих / складних ієрархій прапорів.


Тож ретельні тести парсерного аналізу вирішуватимуть №1. FWIW enum почався як стандартний перелік без прапорців близько 3 років тому. Після втомлення постійно перевіряти наявність якихось конкретних значень (скажімо, в інспекціях коду) з'явилися прапори enum. Цей список також не буде рости з часом, але intвсе-таки є потужність.
Матьє Гіндон

3

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


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

У цій конкретній ситуації поліморфної поведінки між різними типами вузлів дуже мало. Іншими словами, хоча дерево має можливість містити вузли самих різних типів (варіантів), фактичне відвідування цих вузлів в основному вдасться до гігантської ланцюга "тоді ще" (або instanceof/ isперевірки). Ці гігантські перевірки, швидше за все, трапляться в багатьох місцях проекту. Це причина, чому перерахунки можуть здатися корисними або, принаймні, настільки ж корисними, як instanceofі isперевірки.

Модель відвідувачів все ще може бути корисною. Іншими словами, існують різні стилі кодування, які можна використовувати замість гігантського ланцюга instanceof. Однак, якщо ви хочете обговорити різні переваги та недоліки, ви б вирішили продемонструвати приклад коду з найпотворнішої ланцюжки instanceofпроекту, а не посперечатися з приводу перерахунків.

Це не означає, що класи та ієрархія спадкування не корисні. Зовсім навпаки. Хоча немає жодної поліморфної поведінки, яка працює на кожному типі декларацій (окрім того, що кожна декларація повинна мати Nameвластивість), є багато насичених поліморфних форм поведінки, якими поділяються сусідні побратими. Наприклад, Functionі, Procedureймовірно, поділиться деякою поведінкою (як виклику, так і прийняття списку введених вхідних аргументів), і PropertyGetобов'язково успадкує поведінку від Function(обидва мають a ReturnType). Ви можете використовувати або перерахунки, або спадкові перевірки для гігантського ланцюга "тоді ще", але поліморфна поведінка, як би фрагментарна, все ж повинна бути реалізована в класах.

Існує багато порад в Інтернеті проти надмірного використання 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 
}

Ця модифікована версія уникає виділення бітів для конкретних типів декларування. Натомість не конкретні типи декларування (абстрактні угруповання типів декларацій) просто мають значення перерахунків, які є порозрядним або (об'єднанням бітів) для всіх його дітей.

Існує застереження: якщо є абстрактний тип декларування, у якого є одна дитина, і якщо є необхідність відрізняти абстрактне (батьківське) від конкретного (дитина), то абстрактному все одно знадобиться свій власний шматочок .


Одне застереження , що конкретно на це питання: а 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 # (класи в ієрархії спадкування) та вашими значеннями enum.

(Крім того, ви можете налаштувати значення перерахунків, щоб забезпечити зіставлення один на один із типами.)

У C # багато бібліотек зловживають Type object.GetType()чудовим методом - для хорошого чи поганого.

Де б ви не зберігали переліки як значення, ви можете запитати себе, чи можете ви зберегти це 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
    ...
};

Остаточний доказ для тих, хто пропонує класи та ієрархію спадкування ...

Як тільки ви побачите, що перерахунки є наближенням до ієрархії спадкування , дотримуються наступних порад:

  • Створіть (або вдосконаліть) свою ієрархію спадщини,
  • Потім поверніться і спроектуйте свої перерахунки, щоб наблизити цю ієрархію спадкування.

Проект фактично є надбудовою VBIDE - я розбираю і аналізую код VBA =)
Матьє Гіндон

1

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

Прапори - це засіб сигналізації стану, кваліфікації. Якщо я хочу знати, чи щось є фрукти, я знаходжу

штучний і органічний. Плід! = 0

легше читати, ніж

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

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

Дайте цьому хлопцеві кілька очок брауні!


4
thingy is Fruitчитабельніше, ніж будь-який.
ЖакБ
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.