Перечисление (enum) это тип, значения которого образуют фиксированный набор именованных констант: статусы заказа, дни недели, уровни логирования. Внутри каждое имя это целое число, но система типов не даёт перепутать OrderStatus с обычным int или с другим перечислением.
Объявление и использование перечисления
Перечислите имена членов в фигурных скобках. По умолчанию первое равно 0, а каждое следующее на единицу больше:
Вывод:
Paid
On its way
True
2
Перечисление это настоящий тип: метод, принимающий OrderStatus, нельзя по ошибке вызвать с 3 или с LogLevel. Перечисления это типы значений, поэтому они никогда не бывают null и сравниваются через == по значению.
Явные значения и базовый тип
Числа можно назначить самостоятельно. Это важно всякий раз, когда число покидает программу (столбец базы данных, HTTP-статус, формат файла), потому что тогда перенумерация ломает сохранённые данные:
Вывод:
404
Created
418
1
Byte
Обратите внимание на две вещи. Приведение int к перечислению никогда не завершается ошибкой: (HttpStatus)418 это допустимое значение, у которого просто нет имени, и печатается оно как число. А базовым типом может быть любой целочисленный тип (byte, short, long, ...), что важно только для кода, чувствительного к размеру хранения; int это значение по умолчанию и почти всегда правильный выбор.
Добавляя члены позже, добавляйте их в конец или давайте явные значения. Вставка Refunded между Paid и Shipped молча меняет номер каждого члена после него.
Перечисление в строку
ToString() возвращает имя члена, и его же используют Console.WriteLine и интерполяция строк. Строки формата меняют вывод:
Вывод:
Warning
2
00000002
[Warning]
Error
Error
Needs attention
Имена членов это идентификаторы, поэтому в них не может быть пробелов и они не переводятся. Для текста, который видят пользователи, сопоставляйте значения сами, как это делает Label, или через Dictionary<LogLevel, string>. В некоторых кодовых базах на каждый член ставят атрибут [Description("Needs attention")] и читают его через рефлексию; как устроен такой поиск, показано на странице о рефлексии и атрибутах.
Строку в перечисление: Parse и TryParse
Enum.Parse превращает имя обратно в значение и выбрасывает ArgumentException, если ничего не совпало. Enum.TryParse вместо этого возвращает false, и именно это нужно для любого ввода, который вы не контролируете:
Вывод:
Large
Medium
Parse threw ArgumentException for Huge
small parsed=True value=Small defined=True
XL parsed=False value=Small defined=True
2 parsed=True value=Large defined=True
7 parsed=True value=7 defined=False
Последние две строки это ловушка. Оба метода принимают числовые строки, поэтому "7" успешно разбирается в Size, у которого нет имени. А неудачный TryParse присваивает результату 0, который здесь выглядит как допустимый Small. Когда текст приходит из строки запроса, файла конфигурации или формы, всегда проверяйте и возвращаемое значение, и Enum.IsDefined:
if (Enum.TryParse(input, true, out Size size) && Enum.IsDefined(typeof(Size), size))
{
// safe to use size
}
.NET Core 2.0 и новее добавляют обобщённый Enum.Parse<Size>("Large"), которому не нужно приведение.
Перечисление всех значений
Enum.GetValues возвращает все члены, отсортированные по числовому значению (сравниваются как беззнаковые, поэтому отрицательные члены идут последними); Enum.GetNames возвращает их имена. Так заполняют выпадающий список или проверяют значение по всем вариантам:
Вывод:
Free 0 EUR/month
Starter 9 EUR/month
Pro 29 EUR/month
Team 99 EUR/month
Free | Starter | Pro | Team
3 paid plans
Enum.GetValues(typeof(Plan)) возвращает обычный Array, поэтому перед LINQ стоит Cast<Plan>(). В .NET 5 и новее Enum.GetValues<Plan>() сразу возвращает типизированный Plan[].
Флаги: объединение значений
Некоторые перечисления описывают набор вариантов, а не один выбор: права доступа к файлу, дни работы магазина, каналы уведомлений. Дайте каждому члену свой бит (1, 2, 4, 8, ...), добавьте None = 0 и пометьте перечисление [Flags]. Тогда значения объединяются через |:
Вывод:
Read, Share
Editor, Share
True
False
Editor
3
Read, Delete
True
Что делает каждый оператор: | устанавливает биты, & ~X сбрасывает их, ^ переключает, а (value & X) != 0 или value.HasFlag(X) проверяют их. HasFlag(X) означает «установлены все биты X», поэтому HasFlag(None) истинно для любого значения, а HasFlag(Editor) требует и Read, и Write.
Обратите внимание на вторую строку: когда именованная комбинация покрывает часть установленных битов, ToString использует её, поэтому Read | Write | Share печатается как Editor, Share. Помните об этом, прежде чем разбирать вывод ToString чем-то кроме Enum.Parse.
Атрибут не меняет арифметику. Он меняет форматирование: без [Flags] значение Read | Share печатается как 9, потому что ни у одного члена нет такого значения. С ним и ToString, и Parse работают с формой через запятую. Члены по-прежнему должны быть степенями двойки; запись Read, Write, Delete с нумерацией по умолчанию (0, 1, 2) делает Write | Delete равным 3, бессмысленному значению.
switch по перечислению
switch это естественный способ действовать по перечислению. Добавляйте ветку default, потому что переменная перечисления может хранить значения без имени:
switch (status)
{
case OrderStatus.Pending:
case OrderStatus.Paid:
return "Preparing";
case OrderStatus.Shipped:
return "On the way";
case OrderStatus.Delivered:
return "Delivered";
default:
return "Unknown";
}
Начиная с C# 8 switch-выражение короче. Без ветви _ компилятор предупреждает: CS8509, когда пропущен именованный член, и CS8524, когда все имена обработаны, но безымянные значения вроде (OrderStatus)7 нет:
string text = status switch
{
OrderStatus.Pending or OrderStatus.Paid => "Preparing", // 'or' pattern: C# 9
OrderStatus.Shipped => "On the way",
OrderStatus.Delivered => "Delivered",
OrderStatus.Cancelled => "Cancelled",
_ => throw new ArgumentOutOfRangeException(nameof(status)),
};
Значения по умолчанию и неопределённые значения
Значение по умолчанию любого перечисления это 0, независимо от того, есть ли член с таким значением. Его дают поля, элементы массивов и неудачный TryParse. Учитывайте это при проектировании:
- Сделайте
0осмысленным членом «не задано» (None,Unknown), а не реальным вариантом. Иначе неинициализированное поле молча читается как первый реальный вариант. - Проверяйте числа извне через
Enum.IsDefined. Для перечислений[Flags]IsDefinedвозвращаетfalseдля безымянных комбинаций (Read | Share), поэтому проверяйте биты:(value & ~Permissions.All) == 0с членомAll, покрывающим все биты.
Частые ошибки
- Доверие одному
TryParse. Числовые строки разбираются, а неудачный разбор даёт0. ДобавьтеEnum.IsDefined. - Опора на неявную нумерацию для сохраняемых значений. Вставка члена перенумеровывает те, что идут после него. Давайте явные значения любому перечислению, которое сохраняется.
- Флаги не степенями двойки. Нумерация по умолчанию (0, 1, 2, 3) перекрывает биты. Используйте 1, 2, 4, 8 или
1 << n. - Показ
ToString()пользователям. Имена членов это идентификаторы кода. Сопоставляйте значения с отображаемым текстом. - Нет
defaultв switch. Перечисление может хранить значения за пределами именованных членов.
Часто задаваемые вопросы
Как преобразовать enum в строку в C#?
Вызовите ToString(): OrderStatus.Shipped.ToString() возвращает "Shipped", и интерполяция строк делает то же самое. ToString("D") вместо этого даёт число. Для имени, известного при компиляции, nameof(OrderStatus.Shipped) это константа. Для текста для пользователей с пробелами или переводом сопоставляйте значения со строками сами (через switch или словарь), а не полагайтесь на имя члена.
Как преобразовать строку в enum в C#?
Используйте Enum.TryParse<OrderStatus>(text, true, out var status), который возвращает false вместо исключения, когда текст не совпадает ни с одним членом (true делает сравнение нечувствительным к регистру). Enum.Parse(typeof(OrderStatus), text) выбрасывает ArgumentException при плохом вводе. Оба также принимают числовые строки вроде "42", поэтому, когда ввод идёт от пользователей, проверяйте результат через Enum.IsDefined.
Как преобразовать enum в int и обратно в C#?
Приводите в любую сторону: int code = (int)OrderStatus.Paid; и var status = (OrderStatus)2;. Приведение из int никогда не завершается ошибкой, даже для чисел без соответствующего члена; результат это значение перечисления, которое печатается как число. Когда число приходит извне, проверяйте через Enum.IsDefined(typeof(OrderStatus), value).
Как перебрать все значения enum в C#?
foreach (OrderStatus s in Enum.GetValues(typeof(OrderStatus))) посещает все члены в порядке их числовых значений. Начиная с .NET 5 есть обобщённая версия Enum.GetValues<OrderStatus>(), которой не нужно приведение. Enum.GetNames(typeof(OrderStatus)) возвращает имена в виде строк.
Что делает [Flags] у перечисления C#?
Он помечает перечисление, значения которого это биты, предназначенные для объединения через |, например Read | Write. Дайте каждому члену степень двойки (1, 2, 4, 8) и добавьте None = 0. Атрибут заставляет ToString() печатать комбинации как "Read, Write" и позволяет Enum.Parse читать этот формат обратно. Проверяйте бит через HasFlag или (value & Permissions.Write) != 0.