Перечисления enum в C#: объявление и работа
Перечисление enum в C# - это тип, который хранит набор именованных констант: дни недели, статусы заказа, уровни доступа. Вместо магических чисел в коде появляются читаемые имена, а компилятор следит за тем, чтобы значения не путались.
Ниже разобрано, как объявлять enum, менять базовый тип, комбинировать значения через [Flags], перебирать элементы, преобразовывать строки через Enum.Parse и TryParse и использовать перечисления в switch.

Содержание
Объявление enum и значения по умолчанию
Перечисление объявляется ключевым словом enum, после которого идёт имя типа и список членов в фигурных скобках. Если значения не указаны, компилятор нумерует члены с нуля: первый получает 0, второй 1 и так далее. Имя типа принято писать в стиле PascalCase и в единственном числе, например OrderStatus, а не OrderStatuses. Исключение составляют наборы флагов, для которых используют множественное число.
enum OrderStatus
{
New, // 0
Paid, // 1
Shipped, // 2
Delivered, // 3
Cancelled // 4
}
OrderStatus status = OrderStatus.Paid;
Console.WriteLine(status); // Paid
Console.WriteLine((int)status); // 1
Перечисление можно объявить внутри класса или на уровне пространства имён. Если тип нужен нескольким классам, выносите его наружу: так не придётся писать длинное имя вида Order.OrderStatus. Если же enum описывает состояние только одного класса, вложенный вариант держит код компактным.
Любому члену можно присвоить явное значение. Следующие члены без значения продолжат нумерацию от предыдущего. Это удобно, когда числа важны, например, совпадают с кодами в базе данных или в протоколе обмена.
enum HttpCode
{
Ok = 200,
Created = 201,
BadRequest = 400,
Unauthorized, // 401
Forbidden = 403,
NotFound = 404
}
Значение по умолчанию для любого enum равно 0, даже если члена с таким числом в перечислении нет. Поэтому поле типа enum в новом объекте сразу получает нулевое значение. Хорошая практика - сделать нулевым членом осмысленное состояние вроде None, Unknown или New, иначе в коде могут появиться значения, которых вы не задумывали.
Базовый тип перечисления
По умолчанию под enum используется int. Базовый тип указывается через двоеточие после имени. Допустимы целочисленные типы: byte, sbyte, short, ushort, int, uint, long, ulong. Типы char, bool и float не подходят.
enum Permission : byte
{
None = 0,
Read = 1,
Write = 2,
Delete = 4
}
Console.WriteLine(sizeof(Permission)); // 1
Узкий базовый тип экономит память, когда в массиве или в структуре лежит много значений, и нужен, если enum сериализуется в двоичный формат с фиксированной шириной поля. Для флагов, где членов может быть больше 32, берут long или ulong, иначе сдвиги вида 1 << 40 не поместятся.
Преобразование между enum и числом всегда явное. Приведение (int)status даёт число, а (OrderStatus)3 - член перечисления. Компилятор не проверяет, что число входит в набор. Выражение (OrderStatus)42 допустимо, и значение просто напечатается как 42. Проверить принадлежность можно методом Enum.IsDefined.
var s = (OrderStatus)42;
Console.WriteLine(s); // 42
Console.WriteLine(Enum.IsDefined(s)); // False
Console.WriteLine(Enum.IsDefined(OrderStatus.Paid)); // True
Перебор значений и имён
Чтобы получить все члены перечисления, используют Enum.GetValues, а имена - Enum.GetNames. Начиная с .NET 5 есть обобщённые версии, которые возвращают типизированный массив и не требуют приведения.
foreach (OrderStatus s in Enum.GetValues<OrderStatus>())
{
Console.WriteLine($"{(int)s}: {s}");
}
string[] names = Enum.GetNames<OrderStatus>();
Console.WriteLine(string.Join(", ", names));
Такой перебор часто нужен для заполнения выпадающего списка в интерфейсе или для проверки покрытия всех значений в тестах. Если у двух членов одинаковое число, GetNames вернёт оба имени, а GetValues - повторяющиеся значения, о чём стоит помнить при использовании псевдонимов.
Имя члена получают через ToString() или nameof. Первый вариант работает во время выполнения для переменной, второй подставляет строку на этапе компиляции и безопасен при переименовании. Для отображения пользователю имя enum обычно не годится: там нужен русский текст. Его хранят в словаре, ресурсах или атрибуте Description, который читают через рефлексию.
Атрибут [Flags] и побитовые операции
Иногда нужно хранить не одно значение, а набор: у пользователя могут быть права на чтение и запись одновременно. Для этого члены перечисления делают степенями двойки, чтобы у каждого был свой бит, и помечают тип атрибутом [Flags]. Комбинации собираются оператором | (побитовое ИЛИ), проверяются через &, а снимаются через & ~.
[Flags]
enum Access
{
None = 0,
Read = 1,
Write = 2,
Delete = 4,
ReadWrite = Read | Write,
All = Read | Write | Delete
}
Access a = Access.Read | Access.Write;
Console.WriteLine(a); // ReadWrite
Console.WriteLine((a & Access.Write) != 0); // True
Console.WriteLine(a.HasFlag(Access.Delete)); // False
a |= Access.Delete; // добавить флаг
a &= ~Access.Write; // убрать флаг
Console.WriteLine(a); // Read, Delete
Сам атрибут [Flags] не меняет значения членов. Он влияет на текстовое представление: без него комбинация Read | Delete выведется числом 5, а с ним строкой "Read, Delete". Степени двойки вы расставляете вручную. Для удобства при большом числе флагов их записывают через сдвиги: 1 << 0, 1 << 1, 1 << 2.
Метод HasFlag читается проще, чем битовая маска, но для составных значений ведёт себя иначе, чем кажется: HasFlag(Access.ReadWrite) вернёт true только когда установлены оба бита. Для одиночных флагов результат тот же, что у проверки через &. Если нужна проверка "хотя бы один из", используйте (a & mask) != 0.
| Операция | Запись | Результат |
|---|---|---|
| Добавить флаг | a |= Access.Write | бит Write установлен |
| Убрать флаг | a &= ~Access.Write | бит Write сброшен |
| Переключить флаг | a ^= Access.Write | бит меняет значение |
| Проверить флаг | (a & Access.Write) != 0 | true или false |
Член None = 0 в наборе флагов полезен: он обозначает пустой набор. Проверять его через HasFlag бессмысленно, потому что любая комбинация "содержит" ноль, поэтому сравнивайте напрямую: a == Access.None.
Преобразование строки: Enum.Parse и TryParse
Строки из файла настроек, JSON или запроса нужно превращать в enum. Метод Enum.Parse разбирает строку и выбрасывает исключение, если имя неизвестно. Метод Enum.TryParse возвращает bool и не бросает исключений, поэтому для пользовательского ввода подходит именно он. Подробнее о типичных случаях есть в статье про преобразование строки в enum.
OrderStatus s1 = Enum.Parse<OrderStatus>("Paid");
OrderStatus s2 = Enum.Parse<OrderStatus>("paid", ignoreCase: true);
if (Enum.TryParse<OrderStatus>("Shipped", out var s3))
Console.WriteLine($"Разобрано: {s3}");
if (!Enum.TryParse<OrderStatus>("Lost", out _))
Console.WriteLine("Такого статуса нет");
Есть ловушка: TryParse принимает не только имена, но и числа в виде строки. Строка "42" успешно разберётся в OrderStatus, хотя члена 42 нет, а результат будет равен (OrderStatus)42. Поэтому после разбора пользовательских данных добавляйте проверку Enum.IsDefined. Для флагов строка вида "Read, Write" разбирается корректно: имена перечисляются через запятую.
if (Enum.TryParse<OrderStatus>(input, true, out var st) && Enum.IsDefined(st))
{
Console.WriteLine($"Статус: {st}");
}
else
{
Console.WriteLine("Недопустимое значение");
}
Switch по enum
Перечисления хорошо сочетаются с оператором и выражением switch. В классическом варианте на каждый член пишется case, в современном используется switch-выражение, которое возвращает значение. Ветка discard (_) нужна для значений, которых нет в перечислении, ведь, как показано выше, туда может попасть любое число.
static string Describe(OrderStatus s) => s switch
{
OrderStatus.New => "Заказ создан",
OrderStatus.Paid => "Оплачен",
OrderStatus.Shipped => "Передан в доставку",
OrderStatus.Delivered => "Доставлен",
OrderStatus.Cancelled => "Отменён",
_ => throw new ArgumentOutOfRangeException(nameof(s), s, null)
};
Если убрать ветку _, компилятор выдаст предупреждение о том, что switch-выражение не исчерпывает все входные значения. Это защита от забытого члена: добавили новый статус, и IDE показывает места, где его нужно обработать. Для нескольких членов с одинаковым результатом пригодится оператор or: OrderStatus.Delivered or OrderStatus.Cancelled => true.
В классическом switch-операторе у каждого case должен быть break, return или throw. Несколько меток можно объединить подряд, если для них действие общее. Подход к ветвлению разобран и в материалах про методы в C#, куда удобно выносить такие функции-описатели.
Методы расширения и описания для enum
Enum нельзя наделить собственными методами внутри объявления. Для этого пишут методы расширения в статическом классе. Так можно добавить к перечислению проверку допустимых переходов между статусами или получение текста для интерфейса.
static class OrderStatusExtensions
{
public static bool IsFinal(this OrderStatus s) =>
s is OrderStatus.Delivered or OrderStatus.Cancelled;
public static string ToRussian(this OrderStatus s) => s switch
{
OrderStatus.New => "Новый",
OrderStatus.Paid => "Оплачен",
OrderStatus.Shipped => "В пути",
OrderStatus.Delivered => "Доставлен",
OrderStatus.Cancelled => "Отменён",
_ => s.ToString()
};
}
Console.WriteLine(OrderStatus.Shipped.ToRussian());
Console.WriteLine(OrderStatus.Cancelled.IsFinal());
Если перечисление приходит в Web API, по умолчанию в JSON оно сериализуется числом. Чтобы в ответе было имя, подключают JsonStringEnumConverter. Числа в публичном контракте хрупки: порядок членов в коде легко изменить, и клиенты начнут получать другие значения. Поэтому для данных, которые хранятся в базе или передаются наружу, фиксируйте числа явно и не меняйте их после выпуска.
Типичные ошибки при работе с enum
- Не указан нулевой член. Поле получает 0, которому нет имени, и в логах появляется просто число.
- Флаги без степеней двойки. Значения 1, 2, 3 перекрываются битами, и 3 одновременно равно 1 | 2.
- Сравнение с числами. Код вида if ((int)s == 2) нечитаем и ломается при перестановке членов.
- Парсинг без проверки IsDefined. Из "99" получается несуществующий статус.
- Изменение порядка членов в перечислении, сохранённом в базе по числу.
Если набор значений должен нести поведение, например у каждого статуса свои правила и данные, вместо enum лучше взять класс с набором статических экземпляров или иерархию классов. Для простых меток и флагов перечисление остаётся самым лёгким и быстрым вариантом. О выборе типа данных для объектов можно почитать в сравнении структуры и класса.
Частые вопросы
Можно ли наследовать один enum от другого?
Нет. Перечисления в C# не поддерживают наследование, а их базовым типом может быть только целочисленный тип. Если нужно расширить набор значений, создайте новый enum и добавьте преобразование между ними.
Чем отличается Enum.Parse от Enum.TryParse?
Parse выбрасывает ArgumentException или OverflowException при неверной строке, а TryParse возвращает false и записывает значение по умолчанию. Для данных от пользователя используйте TryParse, для заведомо корректных констант допустим Parse.
Нужно ли помечать enum атрибутом [Flags] всегда?
Нет, только когда члены задуманы как комбинируемые биты. Для обычного набора взаимоисключающих состояний атрибут не нужен и только запутает читателя.
Как получить число элементов перечисления?
Выражение Enum.GetValues<T>().Length даёт количество значений, а Enum.GetNames<T>().Length - количество имён. При наличии псевдонимов с одинаковыми числами эти значения могут различаться.
Можно ли использовать enum как ключ словаря?
Да, перечисление подходит в качестве ключа Dictionary. Для небольших плотных наборов иногда быстрее обычный массив, индексируемый приведением (int)значение, но словарь нагляднее и безопаснее.
Как вывести описание вместо имени члена?
Повесьте на член атрибут [Description("Текст")] и прочитайте его через рефлексию, либо используйте метод расширения со switch-выражением, как в примере выше. Второй способ быстрее и проверяется компилятором.





