Enum в C# и некоторые его особенности
За свою непродолжительную карьеру 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 << 0, //0001 = 1
DoSomething = 1 << 1, //0010 = 2
HasAnother = 1 << 2, //0100 = 4
DoAnother = 1 << 3 //1000 = 8
}
}
Как же работать с бинарным флагом? В случае использования отдельных булевых переменных всё достаточно очевидно: обращайся к конкретной переменной и читай или же присваивай значение. Но в случае же работы битовыми флагами приходится использовать базовые двоичные операции: конъюнкция (И, &) и дизъюнкция (ИЛИ, |).
Операция ИЛИ позволяет вернуть множество, содержащее в себе все подмножества, используемые в данной операции:
Операция И позволяет вернуть подмножество пересечения множеств, используемые в данной операции:
Используя эти операции мы можем работать с флагами:
var classWithFlags = new ClassWithFlags();
classWithFlags.Conditions |= ConditionsFlags.DoAnother; //Присвоение значения
var hasClassDoAnother = (classWithFlags.Conditions & ConditionsFlags.DoAnother) != 0; //Чтение значения
hasClassDoAnother = classWithFlags.Conditions.HasFlag(ConditionsFlags.DoAnother); //Более привычное чтение
Как можно увидеть, работа с битовыми флагами является несколько нестандартной. Помимо этого неудобства, битовый флаг подразумевает, что состояние всегда определено и равно либо 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. А при получении значения по числу вернулось первое в списке с таким числом.
Такие вот дела, ребята. А на сегодня всё.