Enum (перечисление) — это тип-значение в C#, заданный набором именованных целочисленных констант. Он ограничивает переменную конечным списком допустимых вариантов и заменяет «магические числа» читаемыми именами: вместо 1 в коде стоит Priority.Low.
Содержание
Ниже разберем, как объявить перечисление, откуда берутся числа под именами, как задать базовый тип, как преобразовывать enum в число и обратно, какие методы дает System.Enum и как из перечисления собрать битовые флаги через атрибут [Flags]. Примеры на C# приведены с ожидаемым выводом для .NET 8.
Как объявить enum
Перечисление объявляют ключевым словом enum и списком имен. По умолчанию базовый тип — int, а значения нумеруются с нуля и растут на единицу в порядке записи.
using System;
enum Season
{
Winter, // 0
Spring, // 1
Summer, // 2
Autumn // 3
}
class Program
{
static void Main()
{
Season s = Season.Summer;
Console.WriteLine(s); // имя члена
Console.WriteLine((int)s); // связанное число
Season d = default; // значение по умолчанию = (Season)0
Console.WriteLine(d);
}
}
Вывод программы:
Summer
2
Winter
Важная деталь про значение по умолчанию: у любого перечисления оно равно (E)0, даже если ни один член не описан числом 0. Здесь 0 занят Winter, поэтому default печатает Winter. Если бы члена с нулем не было, ToString вывел бы просто 0 — это частый источник «пустого» состояния, поэтому нулевой вариант обычно задают явно (например, None = 0).
Явные значения и базовый тип
Числа под именами можно назначить вручную — это нужно, когда значения должны совпадать с внешним контрактом (коды ответа, идентификаторы в базе). Базовый целочисленный тип тоже задается явно, после двоеточия.
using System;
enum HttpStatus : short
{
OK = 200,
NotFound = 404,
ServerError = 500
}
class Program
{
static void Main()
{
HttpStatus code = HttpStatus.NotFound;
Console.WriteLine(code); // NotFound
Console.WriteLine((short)code); // 404
}
}
Вывод программы:
NotFound
404
Базовым может быть любой из целочисленных типов: byte, sbyte, short, ushort, int, uint, long, ulong. По умолчанию берется int. Меньший тип (например, byte) экономит память, если значений мало и они помещаются в диапазон; больший (long) нужен, когда флагов много (см. раздел ниже). Задавать под enum float, string или char нельзя — только целые.
Преобразования: enum и число
Между перечислением и его базовым типом нужно явное приведение — произвольное число C# в enum неявно не пропустит, чтобы случайное значение не попало туда, где ждут именованную константу. Обратная сторона: приведение принимает и число, которого нет среди членов.
using System;
enum HttpStatus : short
{
OK = 200,
NotFound = 404,
ServerError = 500
}
class Program
{
static void Main()
{
HttpStatus unknown = (HttpStatus)418; // такого члена нет
Console.WriteLine(unknown); // печатает число
Console.WriteLine(Enum.IsDefined(typeof(HttpStatus), unknown));
}
}
Вывод программы:
418
False
Приведение (HttpStatus)418 не бросает исключение и не проверяет допустимость — переменная молча получает значение вне списка, а ToString печатает число вместо имени. Именно поэтому значения из внешних источников проверяют методом Enum.IsDefined: он возвращает False для 418 и True для описанных членов. Тихая неверная переменная опаснее явной ошибки, так что вход стоит валидировать до того, как enum уйдет в логику.
Методы System.Enum
Все перечисления наследуют System.Enum — абстрактный базовый класс со статическими помощниками для разбора строк и обхода значений. Самое полезное:
| Метод | Что делает | Пример результата |
|---|---|---|
Enum.GetName<T>(value) |
имя члена по значению | "High" |
Enum.GetValues<T>() |
массив всех значений типа | [Low, Medium, High] |
Enum.Parse<T>(str) |
строка в enum, бросает исключение при промахе | Priority.High |
Enum.TryParse<T>(str, out v) |
безопасный разбор, возвращает bool | False для "Urgent" |
Enum.IsDefined(type, value) |
описан ли такой член | False для 418 |
value.ToString() |
имя члена или число, если не описан | "NotFound" |
Небольшая программа связывает разбор строки, безопасный TryParse и перебор всех значений:
using System;
enum Priority : byte
{
Low = 1,
Medium = 2,
High = 3
}
class Program
{
static void Main()
{
Priority p = Enum.Parse<Priority>("High");
Console.WriteLine((byte)p);
bool ok = Enum.TryParse<Priority>("Urgent", out _);
Console.WriteLine(ok);
foreach (Priority item in Enum.GetValues<Priority>())
Console.WriteLine($"{item} = {(byte)item}");
}
}
Вывод программы:
3
False
Low = 1
Medium = 2
High = 3
Enum.Parse бросит ArgumentException, если строки нет среди членов, поэтому пользовательский ввод разбирают через TryParse: он вернет False и не уронит программу. Обобщенные версии (Parse<T>, GetValues<T>, GetName<T>) доступны начиная с .NET 5 и работают в .NET 8.
Битовые флаги: атрибут [Flags]
Если варианты не взаимоисключающие, а комбинируются (права доступа, набор опций), перечисление помечают атрибутом [Flags], а значениям дают степени двойки. Тогда каждый член занимает отдельный бит, и их складывают побитовым |, а проверяют & или методом HasFlag.
using System;
[Flags]
enum Access
{
None = 0,
Read = 1,
Write = 2,
Execute = 4,
All = Read | Write | Execute // 7
}
class Program
{
static void Main()
{
Access rights = Access.Read | Access.Write;
Console.WriteLine(rights); // комбинация имен
Console.WriteLine((int)rights); // битовая маска
Console.WriteLine(rights.HasFlag(Access.Write));
Console.WriteLine(rights.HasFlag(Access.Execute));
rights |= Access.Execute; // добавили право
Console.WriteLine(rights);
rights &= ~Access.Read; // сняли право
Console.WriteLine(rights);
}
}
Вывод программы:
Read, Write
3
True
False
All
Write, Execute
Атрибут [Flags] меняет только ToString: значение 3 печатается как Read, Write, а не как число. Когда маска совпала с составным членом All = 7, вывод сворачивается в одно имя All. Снятие бита делают через & ~Флаг: 7 & ~1 = 6, то есть Write, Execute.
Почему именно степени двойки
Флаги обязаны быть степенями двойки (1, 2, 4, 8, …), иначе биты пересекутся и комбинации совпадут с чужими членами. Вот типичная ошибка — Execute = 3:
using System;
[Flags]
enum Bad
{
Read = 1,
Write = 2,
Execute = 3 // 3 не степень двойки: это Read | Write
}
class Program
{
static void Main()
{
Bad combo = Bad.Read | Bad.Write; // 1 | 2 = 3
Console.WriteLine(combo);
Console.WriteLine(combo.HasFlag(Bad.Execute));
}
}
Вывод программы:
Execute
True
Комбинация Read | Write дала 3, а Execute тоже равен 3 — и код решил, что выставлено право Execute, которое никто не выдавал. HasFlag(Bad.Execute) возвращает True по ошибке. Исправление — вернуть степени двойки: Read = 1, Write = 2, Execute = 4, тогда маски не пересекаются и каждое право занимает свой бит.
Ограничения и типовая роль
Внутри перечисления нельзя объявлять методы — добавить поведение можно только методом-расширением над типом enum. Само перечисление — тип-значение, поэтому оно удовлетворяет ограничению struct и не допускает null; а начиная с C# 7.3 в обобщениях доступно ограничение where T : Enum.
На практике enum берут там, где переменная принимает один из фиксированного набора вариантов: состояние заказа, роль пользователя, направление хода, режим работы. Такой код читается лучше строк и чисел, а компилятор ловит опечатки: несуществующего члена просто нет.
Выводы
- Enum — тип-значение из именованных целочисленных констант: заменяет «магические числа» именами и ограничивает переменную конечным списком вариантов.
- Значения нумеруются с нуля по порядку записи; значение по умолчанию всегда
(E)0, поэтому нулевой вариант (None = 0) задают явно. - Базовым может быть любой целочисленный тип (byte..ulong, по умолчанию int); преобразование enum и числа только явное и не проверяет допустимость — вход валидируют через
Enum.IsDefined. System.Enumдает разбор и обход значений (Parse/TryParse,GetValues,GetName); пользовательский ввод безопаснее разбирать черезTryParse.- Атрибут
[Flags]со значениями-степенями двойки хранит комбинации: их складывают|, а проверяют&илиHasFlag; не-степень двойки ломает маски.
Где применяется и что учить дальше
Перечисления — базовый инструмент типобезопасного кода на C#: они убирают «магические числа», делают switch по состояниям наглядным и через [Flags] компактно хранят наборы опций. Уверенное владение enum, приведениями типов и System.Enum — часть фундамента, без которого тяжело идут коллекции, LINQ и работа с библиотеками .NET.
Освойте тему на практике
Если хочется освоить C# системно — от синтаксиса и типов данных до .NET и практических проектов, посмотрите курс C# Basic. Оценить формат и уровень до старта помогают бесплатные открытые уроки Otus — живые занятия с преподавателями.
FAQ
Можно ли перебрать все значения enum в цикле? Да, через Enum.GetValues<T>() — он возвращает массив всех членов, по которому проходят foreach. Для получения имен по значениям есть Enum.GetName<T> и Enum.GetNames<T>.
Что будет, если привести к enum число вне списка? Приведение пройдет без ошибки, и переменная получит значение вне набора членов; ToString напечатает само число. Проверять допустимость нужно вручную через Enum.IsDefined.
Чем enum отличается от набора констант const? Enum — это отдельный тип: переменная принимает только его члены, компилятор ловит чужие значения, а [Flags] и методы System.Enum работают из коробки. Набор const int такой проверки типа и удобного перебора не дает.



