Когда использовать перечисления, а когда заменить их классом со статическими членами?

Недавно мне пришло в голову, что следующее (примерное) перечисление...

enum Color
{
    Red,
    Green,
    Yellow,
    Blue
}

... можно заменить, казалось бы, более типобезопасным классом:

class Color
{
    private Color() { }

    public static readonly Color   Red      = new Color();
    public static readonly Color   Green    = new Color();
    public static readonly Color   Yellow   = new Color();
    public static readonly Color   Blue     = new Color();
}

Под «типобезопасностью» я подразумеваю, что следующий оператор будет работать, если Color будет перечислением, но не будет, если Color будет вышеуказанным классом:

var nonsenseColor = (Color)17;    // works if Color is an enum

Два вопроса:

1) Существует ли общепринятое название для этого шаблона (замена перечисления типобезопасным классом)?

2) В каких случаях следует использовать перечисления, а когда лучше использовать класс?


person Community    schedule 22.01.2010    source источник
comment
Я не уверен, что существует именованный шаблон для замены перечисления классом, но лично я не вижу дополнительной ценности. Enum прост и эффективен для таких случаев, как выше.   -  person KP.    schedule 22.01.2010
comment
Перечисления в Java аналогичны тому, чего вы пытаетесь достичь.   -  person empi    schedule 22.01.2010
comment
Одно из преимуществ подхода «перечисление как класс» состоит в том, что вы не можете присваивать значения, выходящие за пределы допустимого диапазона, как я это продемонстрировал. Я не уверен, есть ли помимо этого больше преимуществ или есть ли вообще недостатки - вот что я хотел бы выяснить с помощью этого вопроса.   -  person stakx - no longer contributing    schedule 22.01.2010
comment
Перечисления очень дешевы, они являются типами значений.   -  person Hans Passant    schedule 22.01.2010
comment
@stakx: вы также можете добиться такой же безопасности типов, используя структуру вместо класса, и достичь цели сохранения небольшого/дешевого объекта данных, который живет в стеке. IMO перечисление является самым безопасным. Заявление, от которого вы пытаетесь защититься, — это просто плохой код.   -  person Joel Etherton    schedule 22.01.2010
comment
@stakx: Наверное, я просто немного более безжалостен. Если бы кто-то неправильно использовал мои перечисления, я бы сказал, что он заслужил сопутствующее ему горе ›:)   -  person Joel Etherton    schedule 22.01.2010


Ответы (6)


Перечисления отлично подходят для легковесной информации о состоянии. Например, ваше перечисление цветов (исключая синий) подойдет для запроса состояния светофора. Истинный цвет вместе со всей концепцией цвета и всем его багажом (альфа, цветовое пространство и т. д.) не имеет значения, просто в каком состоянии находится свет. Кроме того, немного изменив ваше перечисление, чтобы представить состояние светофора :

[Flags()]
public enum LightColors
{
    unknown = 0,
    red = 1,
    yellow = 2,
    green = 4,
    green_arrow = 8
}

Текущее состояние света может быть установлено как:

LightColors c = LightColors.red | LightColors.green_arrow;

И запрошен как:

if ((c & LightColors.red) == LightColors.red)
{
    //Don't drive
}
else if ((c & LightColors.green_arrow) == LightColors.green_arrow)
{
    //Turn
}

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

Однако статические члены класса прекрасно подходят для часто используемых объектов. Члены System.Drawing.Color являются отличными примерами, поскольку они представляют цвета с известными именами, которые имеют неясные конструкторы (если только вы не знаете свои шестнадцатеричные цвета). Если бы они были реализованы как перечисления, вам пришлось бы делать что-то подобное каждый раз, когда вы хотите использовать значение в качестве цвета:

colors c = colors.red;
switch (c)
{
    case colors.red:
        return System.Drawing.Color.FromArgb(255, 0, 0);
        break;
    case colors.green:
        return System.Drawing.Color.FromArgb(0,255,0);
        break;
}

Итак, если у вас есть перечисление и вы обнаружите, что постоянно выполняете переключатель/случай/если/иначе/что угодно для получения объекта, вы можете использовать статические члены класса. Если вы запрашиваете только состояние чего-либо, я бы придерживался перечислений. Кроме того, если вам приходится передавать данные небезопасным способом, перечисления, вероятно, выживут лучше, чем сериализованная версия вашего объекта.

Редактировать: @stakx, я думаю, вы тоже наткнулись на что-то важное, в ответ на сообщение @Anton, и это сложность или, что более важно, для кого это сложно?

С точки зрения потребителя я бы предпочел статические члены класса System.Drawing.Color, а не необходимость писать все это. Однако с точки зрения продюсера писать все это было бы мучением. Поэтому, если другие люди будут использовать ваш код, вы можете избавить их от многих проблем, используя статические члены класса, даже если вам потребуется в 10 раз больше времени для написания/тестирования/отладки. Однако, если это только вы, вам может быть проще просто использовать перечисления и приводить/преобразовывать по мере необходимости.

person Community    schedule 22.01.2010
comment
Хороший ответ, Крис. Я принял ваш ответ, потому что чувствую, что он прекрасно сочетает в себе несколько хороших моментов. - person stakx - no longer contributing; 22.01.2010
comment
Перечисления великолепны, мы должны снова сделать перечисления великолепными. - person Andreas; 17.05.2017
comment
Что такое [Flags()] над Enum и что он делает? - person dsanchez; 15.08.2019
comment
@dsanchez [Flags()] — это атрибут, указывающий, что набор значений внутри можно рассматривать как набор битовых флагов. Этот пример из документации MSDN демонстрирует использование атрибута [Flags()] чтобы вернуть несколько значений перечисления с помощью побитового ИЛИ - person The Fluffy Robot; 26.01.2020

Я действительно боролся с чем-то подобным на работе.

Он был основан на примере, который был опубликован Джон Б в статье блога Джона Скита "Расширенные перечисления в C#"< /а>.

То, что я не смог нормально работать без МНОГО уродства, — это расширение перечисления путем создания базового класса перечисления и добавления дополнительных статических полей производного типа.

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

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

Другой проблемой была сериализация Xml, которую мы часто используем. Я обошел это, используя два универсальных класса в качестве типов данных для переменных перечисления в наших бизнес-объектах: один, который представлял одно значение перечисления, и один, который представлял набор значений перечисления, которые я использовал для имитации поведения перечисления флагов. Они обрабатывали XML-сериализацию и десериализацию фактических значений, которые были сохранены в свойствах, которые они представляли. Пара перегрузок операторов была всем, что было необходимо, чтобы воссоздать поведение битового флага таким образом.

Это некоторые из основных проблем, с которыми я столкнулся.

В общем, я не очень доволен конечным результатом, там есть какой-то вонючий материал, но пока он работает.

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

Здесь нет никакого кода, и я не смогу добраться до него на выходных, иначе я мог бы показать, куда я пошел с ним. Хотя не очень красиво... :o

person Community    schedule 22.01.2010
comment
Интересная статья в блоге, на которую вы ссылаетесь! -- Одна вещь, которая меня немного беспокоила в подходе перечисления как класса, заключалась в том, что код для простого enum выглядит намного лучше. Реализация перечисления в виде класса может сильно усложнить код, и, ИМХО, возможно, быстро достигается точка, в которой он становится слишком сложным? - person stakx - no longer contributing; 22.01.2010
comment
Код самих перечислений совсем не сложен. У вас есть общий класс, чтобы поместить все основные функции, которые вам требуются от ваших расширенных перечислений. Затем сами перечисления сводятся не более чем к статическим полям и частному конструктору. Плюс любая конкретная функциональность для этого перечисления. Я потратил большую часть времени на то, чтобы позволить разработчикам использовать их как обычные перечисления/битовые флаги. Но да, у вас должна быть очень веская причина, чтобы начать использовать подобные перечисления. - person Anton; 22.01.2010

Кое что нашел за это время, если кому интересно:

  • Блоки switch не будут работать с перечислением-как-классом.

  • Пользователь empi упомянул сходство приведенного выше примера перечисления как класса с перечислениями Java. Похоже, что в мире Java существует общепризнанный шаблон под названием Typesafe Enum; очевидно, этот шаблон восходит к книге Джошуа Блоха Effective Java.

person Community    schedule 22.01.2010
comment
ИМО, switch — это анти-шаблон, потому что он создает несколько точек соприкосновения при добавлении значения перечисления. Код в switch должен быть методом класса. - person Doug Domeny; 29.03.2018

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

public enum Legacy : ushort
{
  SomeFlag1 = 0x0001,
  SomeFlag2 = 0x0002,
  // etc...
}

Тогда сортировка в P/Invoke менее удобочитаема из-за приведения, необходимого для преобразования перечисления в соответствующее значение.

Если бы это была константная переменная класса, приведение не требовалось бы.

person Community    schedule 22.01.2010

Оба подхода действительны. Вы должны выбрать в каждом конкретном случае.

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

Существует очень похожий рефакторинг под названием «Замена перечислений шаблоном стратегии». Ну, в вашем случае это не совсем полно, так как ваш Цвет пассивен и не действует как стратегия. Однако почему бы не рассматривать это как частный случай?

person Community    schedule 22.01.2010

Да, оригинальное издание «Effective Java» Джошуа Блоха, которое было выпущено до Java 1.5 и встроенной поддержки перечислений в Java, продемонстрировало шаблон Typesafe Enum. К сожалению, последний выпуск книги ориентирован на Java > 1.5, поэтому вместо него используются перечисления Java.

Во-вторых, вы не можете преобразовать int в enum в Java.

Color nonsenseColor = (Color)17; // compile-error

Хотя я не могу говорить за другие языки.

person Community    schedule 22.01.2010