Когда использовать перечисления, а когда заменить их классом со статическими членами? - PullRequest
32 голосов
/ 22 января 2010

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

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();
}

С "type-safe" я имею в виду, что следующее утверждение сработало бы, если бы Color было перечислением, но не если бы Color было вышеуказанным классом:

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

Два вопроса:

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

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

Ответы [ 6 ]

26 голосов
/ 22 января 2010

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

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

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

Однако члены класса static прекрасно подходят для часто используемых объектов. 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;
}

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

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

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

3 голосов
/ 22 января 2010

Я действительно довольно долго боролся с чем-то вроде этого.

Он основан на примере, опубликованном Джоном Б в статье в блоге Джона Скита "Enhanced enums in C #" .

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

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

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

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

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

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

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

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

2 голосов
/ 22 января 2010

Некоторые вещи, которые я нашел тем временем, если кому-то еще интересно:

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

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

1 голос
/ 22 января 2010

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

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

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

1 голос
/ 22 января 2010

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

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

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

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

0 голосов
/ 22 января 2010

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

Во-вторых, вы не можете приводить от int к enum в Java.

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

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

...