Menu

Перечисления enum в C#: значения, ToString, Parse, Flags и перебор

Как работают перечисления в C#: объявление именованных констант, целочисленные значения и приведение, преобразование enum в строку и строки в enum через Parse и TryParse, перечисление всех значений, [Flags] с побитовыми операторами и HasFlag, switch по enum и обработка неопределённых значений.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Перечисление (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.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ