Перечисления (enum) в C#: объявление, значения, преобразования и флаги

Перечисления (enum) в C#: объявление, значения, преобразования и флаги Полезное

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 такой проверки типа и удобного перебора не дает.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)