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

Что же такое enum и на кой он вообще нужен?

Представим что нам необходимо определить такое свойство класса как цвет. Как же нам решить эту проблему?

Мы можем сделать это через строковую переменную:

public class ClassWithString
{
    public ClassWithString(string color)
    {
        Color = color;
    }

    public string Color { get; }

    public bool IsDefinedColor(string otherColor)
        => Color.Equals(otherColor);
}
var redClass = new ClassWithString("Red");

var check = redClass.IsDefinedColor("Red");         //true
var secondCheck = redClass.IsDefinedColor("Blue");  //false

В результате выполнения кода значение check будет равно true, а secondCheck будет равно false. Вроде бы задача решена, в прод. Но человек существо не идеальное, и может случиться такое, что в метод будет передано не Red, а red или rad. Вроде-бы человек может догадаться что было ему сказано, но машина прямолинейна и догадываться не будет. В результате некорректного ввода метод будет возвращать false, хотя мы ожидаем true:

var redClass = new ClassWithString("Red");

var check = redClass.IsDefinedColor("Red");         //true
var secondCheck = redClass.IsDefinedColor("blue");  //false

var thirdCheck = redClass.IsDefinedColor("red");    //false
var fourthCheck = redClass.IsDefinedColor("rad");   //false

Конечно первый вид опечатки можно исправить добавлением в Equals дополнительного параметра StringComparison.OrdinalIgnoreCase. Но этого не сделать при второй опечатке. Значит строки нам не особо подходят.

Color.Equals(otherColor, StringComparison.OrdinalIgnoreCase);

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

//0 - red
//1 - blue
public class ClassWithInt
{
    public ClassWithInt(int color)
    {
        Color = color;
    }

    public int Color { get; }

    public bool IsDefinedColor(int otherColor)
        => Color.Equals(otherColor);
}

Вроде-бы проблема решена: теперь никто не опечатается при вводе кода цвета. Но здесь всплывает другая проблема: человек не машина, и работать со словами ему на порядок проще чем с числами. Постоянно помнить, что 0 - это красный, а 1 - это синий, никакой памяти не напасёшься. А если человек поменяет проект и под номером 0 будет белый, а под 1 черный? Ерунда какая-то!!!

И тут нам на помощь приходит генная инженерия, позволяющая нарушить базовые законы колдовства и естества. Мы можем скрестить числа и строки!!! После столь противоестественного действия мы и получим любимый мною enum.

Enum-ом в базовом понимании называют некий список возможных, заранее определенных, именованных значений. Нужен этот список для того, чтобы упростить работу разработчику в оперировании некими состояниями. Решим нашу задачу с применением enum.

public class ClassWithEnum
{
    public ClassWithEnum(Colors color)
    {
        Color = color;
    }

    public Colors Color { get; }

    public bool IsDefinedColor(Colors otherColor)
        => Color.Equals(otherColor);

    public enum Colors
    {
        Red,
        Blue
    }
}

Как можно увидеть внизу нашего класса мы определили тот самый enum и сказали, что он может принимать 2 значения: Red и Blue. Теперь повторим вызов из примера со строками:

var redClass = new ClassWithEnum(ClassWithEnum.Colors.Red);

var check = redClass.IsDefinedColor(ClassWithEnum.Colors.Red);         //true
var secondCheck = redClass.IsDefinedColor(ClassWithEnum.Colors.Blue);  //false

Как можно заметить, всё понятно, ясно, чЁтКо. А теперь попытаемся опечататься:

var redClass = new ClassWithEnum(ClassWithEnum.Colors.Red);

var check = redClass.IsDefinedColor(ClassWithEnum.Colors.Red);         //true
var secondCheck = redClass.IsDefinedColor(ClassWithEnum.Colors.Blue);  //false

var thirdCheck = redClass.IsDefinedColor(ClassWithEnum.Colors.red);    //Ошибка ввода
var fourthCheck = redClass.IsDefinedColor(ClassWithEnum.Colors.rad);   //Ошибка ввода

Редактор кода говорит, что я дурачок и не умею правильно писать слова. А это значит, что я могу заметить ошибку ещё до сборки и гарантированно её замечу во время сборки. Спасибо, добрый блокнот.

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

Enum это число!1!1!1

Как было сказано выше enum - это, по сути, плод запретной любви строки и числа. От строки, как было показано, опять же, выше enum получил отличную человекочитаемость. От числа enum получил устойчивость к неправильному вводу. Но, помимо этого, от числа enum получил и внутренности.

По сути enum это обыкновенное число с табличкой. В примере выше это было не явно потому, что в списке были только имена, но не было чисел. Это не совсем правильный, хотя и рабочий по причине автопроставления чисел, подход к определению enum-а. Более правильным будет следующий подход:

public enum Colors
{
    Red = 0,
    Blue = 1
}

Из того, что enum - это число с бейджиком, следует несколько особенностей:

  • Возможность хранения нескольких булевых свойств в битовых флагах

  • Возможность приведения числа к enum.

  • Особенное приведение одного enum-а к другому.

Разберем эти особенности:

Битовые флаги

Предположим мы имеем некий объект, обладающий рядом булевых свойств. Мы, несомненно, можем описать их в виде нескольких самостоятельных полей типа bool. Данный подход является вполне приемлемым:

public class ClassWithBools
{
    public ConditionsBools Conditions { get; set; }
    
    public class ConditionsBools
    {
        public bool HasSomething { get; set; }
        public bool DoSomething { get; set; }
        public bool HasAnother { get; set; }
        public bool DoAnother { get; set; }
    }
}

Однако, есть иной способ определить эти атрибуты. Для этого мы можем использовать такую надстройку над enum-ом, как битовые флаги. Подобная конструкция представляет из себя обыкновенное enum-ом, обладающее атрибутом [Flags] и числовыми значениями, представляющими из себя степени двойки:

public class ClassWithFlags
{
    public ConditionsFlags Conditions { get; set; }
    
    [Flags]
    public enum ConditionsFlags
    {
        HasSomething = 1,   //0001
        DoSomething = 2,    //0010
        HasAnother = 4,     //0100
        DoAnother = 8       //1000
    }
}

Как же работать с бинарным флагом? В случае использования отдельных булевых переменных всё достаточно очевидно: обращайся к конкретной переменной и читай или же присваивай значение. Но в случае же работы битовыми флагами приходится использовать базовые двоичные операции: конъюнкция (И, &) и дизъюнкция (ИЛИ, |).

Операция ИЛИ позволяет вернуть множество, содержащее в себе все подмножества, используемые в данной операции:

Демонстрация дизъюнкции множеств П и У на картинках из интернетов
Демонстрация дизъюнкции множеств П и У на картинках из интернетов

Операция И позволяет вернуть подмножество пересечения множеств, используемые в данной операции:

Демонстрация конъюнкции множеств П и У на картинках из интернетов
Демонстрация конъюнкции множеств П и У на картинках из интернетов

Используя эти операции мы можем работать с флагами:

var classWithFlags = new ClassWithFlags();
classWithFlags.Conditions |= ConditionsFlags.DoAnother;                 //Присвоение значения
bool hasClassDoAnother = (Conditions & ConditionsFlags.DoAnother) != 0; //Чтение значения

Как можно увидеть, работа с битовыми флагами является несколько нестандартной. Помимо этого неудобства, битовый флаг подразумевает, что состояние всегда определено и равно либо true, либо false, и не может быть равно null. Данная особенность может являться недостатком, поскольку порой постановка задачи может подразумевать, что состояние может быть неизвестно.

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

Приведение числа к enum

В ходе решения некой задачи нам может понадобиться на время расширить допустимый диапазон значений enum, но вот незадача, мы не хотим чтобы дополнительные значения были видны извне написанного нами кода, или же мы не имеем доступа к редактированию enum по причине того, что он не наш и поставляется нам сторонней библиотекой. Как же быть, спросите вы. А всё легко и просто, следите за руками.

public class ClassWithEnumCast
{
    private const Colors TempColor = (Colors)int.MaxValue;

    private readonly Colors[] _colorsOrder = { Colors.Red, TempColor, Colors.Black, Colors.White, Colors.Blue };

    public IEnumerable<Colors> GetOrder(Colors color)
        => _colorsOrder.Select(x =>
            x is TempColor
                ? color
                : x
        ).Distinct();

    public enum Colors
    {
        Red = 0,
        Blue = 1,
        White = 3,
        Black = 4
    }
}

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

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

var color = (Colors)1;          //Blue
var undefinedColor = (Colors)5; //5

Приведение одного enum к другому

Представим, что нам необходимо привести один enum к другому, например при маппинге одной структуры к другой. Как же поведет себя enum? Определим пару enum-ов и попробуем это сделать:

public enum Colors1
{
    Red = 0,
    Blue = 1
}
public enum Colors2
{
    White = 0,
    Red = 1,
    Blue = 2
}

В представленном примере мы создали два enum-а, один из которых обладает всеми значениями второго, но со смещенной из-за дополнительного значения нумерацией. Теперь попробуем привести первый enum ко второму:

var color = (Colors2)Colors1.Red;   //White

Но как же так, вроде же мы должны были получить значение Red, но получили White? Всё дело в том, что enum, как было сказано выше, является лишь именованным числом и приведение происходит именно по числовому значению. В случае необходимости подобного приведения потребуется написать дополнительный хелпер или явно описать каст через implicit/explicit.

public static Colors2 Cast(this Colors1 color)
    => color switch
    {
        Colors1.Red => Colors2.Red,
        Colors1.Blue => Colors2.Blue,
        _ => Colors2.White
    };

Бонусный контент

Подобный пример был создан на основе комментария с просторов Метанита. Предположим мы решим в край упороться и написать следующий enum (так делать не надо, пнятнеько):

enum Colors
{
    White,
    Black = 0,
    Blue = 3,
    Green = 2,
    Red
}

А затем посмотреть как он будет себя вести:

var color1 = Colors.Black;  //White
var color2 = Colors.Red;    //Blue

Что же за магия происходит в этом примере?

Поскольку у значения White явно не указано число, ему автоматически предоставляется число предыдущего значения +1, а поскольку это значение первое и перед ним ничего нет ему будет проставлено значение 0. Далее мы явно указываем для Black значение 0, что дублирует уже предоставленное значение для White. А поскольку White определено до Black, то и при обращении по значению 0 мы получим именно White.

В случае же color2, из-за описанной выше логики автопроставления чисел, значению Red было проставлено число 2+1=3. А при получении значения по числу вернулось первое в списке с таким числом.

Такие вот дела, ребята. А на сегодня всё.

Комментарии (21)


  1. gdt
    25.11.2022 14:51
    +4

    Спасибо за статью, но если делаете туториал, то лучше давать сразу best practices. Например:

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

    А можно использовать string.Compare(str1, str2, StringComparison.OrdinalIgnoreCase).

    Для проверки того, определено ли значение enum - лучше использовать Enum.IsDefined. Если нужно огласить весь список - Enum.GetValues. Также ничего не сказано про флаги, и про приколы с боксингом для методов Enum, и почему оно вообще так было сделано. Также очень интересно, что enum не реализует IEquatable / IComparable, что ограничивает его применимость в дженериках (см например https://github.com/dotnet/runtime/issues/17456).


    1. Orange_Kir Автор
      25.11.2022 17:13

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


  1. GavriKos
    25.11.2022 15:56
    +4

    Я бы предложил дополнить статью очень большим коварством енама - его сериализацией.
    Некоторые сериализаторы могут сериализовать enum как число. И вот тут будет большой косяк, если в ходе обновления кода в старый енам добавится новое значение где то в середине (что вполне норма если енам упорядочен по какой то логике для красоты) - все старые сериализованные данные съедут напрочь.


    1. dopusteam
      25.11.2022 20:22

      если в ходе обновления кода в старый енам добавится новое значение где то в середине

      Жесть какая то, думаю никто в здравом уме такого делать не будет.

      Если речи идёт об изменении значения, а не просто о вставке нового значения между уже существующими (типа добавить 35 между 30 и 40)


  1. lair
    25.11.2022 18:01
    +1

    Код скриншотами - это восторг, конечно.

    Не раскрыта куча всего интересного, например, самое банальное - почему в методе X(SomeEnum v) на входе может оказаться значение, которое не входит в перечисление SomeEnum (и поэтому в switch нужно иметь default-ветку).


    1. Orange_Kir Автор
      25.11.2022 18:13

      По коду на скринах согласен, не лучшее решение, постараюсь это поправить. Про дополнительные значения перечисления я прямо указывал в куске про каст числа в енам, если я правильно понял суть претензии (никаких негативных коннотаций). Необходимость проставления default в switch где-либо (в том числе для енама) я воспринимаю как саму собой разумеющуюся конструкцию.


      1. lair
        25.11.2022 18:19
        -1

        Про дополнительные значения перечисления я прямо указывал в куске про каст числа в енам

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

        Необходимость проставления default в switch где-либо (в том числе для енама) я воспринимаю как саму собой разумеющуюся конструкцию.

        ...понимаете ли, в чем дело - разные люди разные вещи воспринимаеют как "само собой разумеющееся". Для меня вот все, что написано в вашем посте про энамы - само собой разумеющееся, кроме куска про, что будет если перемешать явные и неявные значения (потому что у меня это проходит по категории "не надо так делать, поведение не определено"). А кто-то не понимает, почему нужна ветка, захватывающая неизвестные значения. А кто-то не понимает, почему иногда можно обойтись без default.


        1. Orange_Kir Автор
          25.11.2022 18:26
          -2

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


          1. lair
            25.11.2022 18:31

            Она скорее про некоторые интересные штуки, обнаруженные мою в ходе работы с перечислениями.

            ...тогда зачем нужна вся остальная статья, которая этих "штук" не касается?


            1. Orange_Kir Автор
              25.11.2022 18:42

              Сам по себе енам для меня уже является "интересной штукой", если уж на то пошло. Да и надо было с чего-то начать статью, вот я и решил дать небольшую вводную.


              1. lair
                25.11.2022 18:44
                -1

                То, что что-то интересно для вас, еще не значит, что оно интересно всем остальным (и при этом еще не описано где-то в другом месте, лучше и подробнее).


                1. Orange_Kir Автор
                  25.11.2022 18:54

                  Я не хочу оскорбить вас своим комментарием, но, по-моему, текст перед скобочками немного не сочетается с текстом непосредственно в скобках (возможно дело в опечатке в месте "еще не описано"). К вопросу о том, что другим этот текст может быть не интересен: моё дело предложить, а публика пусть уже выбирает. Ваш выбор я услышал, я его уважаю и не хочу его оспаривать. Еще раз спасибо за критику по поводу скринов, я к ней прислушался


                  1. lair
                    25.11.2022 18:55
                    -1

                    Я не хочу оскорбить вас своим комментарием, но, по-моему, текст перед скобочками немного не сочетается с текстом непосредственно в скобках

                    Текст в скобках относится к случаю, когда что-то, вам интересное, действительно интересно другим - возможно оно уже описано кем-то еще (в случае с enum это "возможно" практически гарантированно).


  1. yerm
    25.11.2022 20:46
    +2

    Я попал в десадовскую группу? Ой, извините.


  1. AirLight
    25.11.2022 21:50

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


    1. s207883
      28.11.2022 10:16

      Некоторые бд поддерживают работу с перечислениями


  1. LightVersion
    25.11.2022 22:03

    Какой-нибудь SmartEnum наше всё :)


  1. nronnie
    26.11.2022 04:07
    -5

    на кой он вообще нужен?

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


  1. glazzkoff
    27.11.2022 10:34

    Enum в C# хорошенько так позволяет выстрелить себе в ногу. Надеюсь у автора за всю карьеру ноги были целыми ????

    А в целом статья интересная, многое из этого я не знал. Спасибо!


  1. Le0Wolf
    27.11.2022 23:00
    +2

    1) Задавать цвета энамом все же не лучшая идея, если у вас конечно не строго ограничены их варианты и новых иметь никогда не планируется. Если уж очень хочется, то тут на хабре была статья про строгую типизацию, где предлагалось на каждую сущность создавать объект, соответственно тут можно было бы инкапсулировать цвет в объекте Color, который бы, например, инициализировался числовыми значениями для red, green и blue.

    2) в случае с енамами городить отдельный метод IsDefindedXXX нет смысла, так как значения энамов проверяются на этапе компиляции, а если значение передаётся строкой, то у класса Enum есть специальный метод

    3) битовые флаги проще указывать через битовые сдвиги типа 2 << 1, в этом случае не придётся вычислять значение самостоятельно

    4) Для проверки флагов есть метод Enum.HasFlag, нет смысла городить огород с би овыми операциями, с ним код и гораздо читабильнее

    5) Флаги все же сущность из мира таких языков, как С++, и нужна по большому счету только для оптимизации. В абсолютном большинстве случаев лучше использовать объект с boolean свойствами, даже в плане понятности и удобства использования

    6) Раздел "Приведение числа к Enum" абсолютно непонятен, поскольку заголовок про одно, текст про другое, а пример и вовсе про третье. В целом, число к перчислению приводится обычной операцией приведения типа, задача расширения перечисления без его изменения признак плохой архитектуры, а задачи сортировки перечислений (без привязки к какой либо метаинформации) могут возникнуть только в случае, если перечисление используется не по назначению.

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

    В целом статья про то, как не надо использовать перечисления


  1. arTk_ev
    28.11.2022 02:11

    Все это очень плохая практика, так делать нельзя.

    • Нельзя расширять просто так энумы, нужно править каждое использование. Энумы - .

    • Нельзя использовать строки в коде, это данные.

    • Не нужно костылить энумы, так это неочевидное усложнение.

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