Ardalis.SmartEnum для C# (.NET): полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Ardalis.SmartEnum для C# (.NET): полное руководство с примерами

1. Что это и зачем нужна

Ardalis.SmartEnum — это библиотека для C# (.NET), которая решает одну из самых распространённых проблем в разработке: неудобство работы с перечислениями (enum). Встроенные в C# перечисления — это, по сути, набор целочисленных констант. Они не могут содержать логику, дополнительные поля (например, описание, код валюты, цвет) или методы. Это приводит к раздуванию кода с помощью switch или if-else, что нарушает принципы SOLID (в частности, Open/Closed Principle) и делает код менее поддерживаемым.

SmartEnum предлагает альтернативу: умные перечисления (Smart Enums). Это классы, которые наследуются от базового класса SmartEnum<TEnum, TValue>. Каждое значение такого перечисления — это статическое поле-синглтон. Благодаря этому, вы можете добавлять в перечисление любые свойства (Name, Value — обязательны, плюс любые кастомные), методы и даже реализовывать интерфейсы. Это превращает перечисление из простого набора чисел в полноценный объект с поведением.

Зачем это нужно? Во-первых, для типобезопасности. Вы не сможете случайно присвоить переменной значение, которое не определено в перечислении (в отличие от обычных enum, где можно сделать (MyEnum)42). Во-вторых, для инкапсуляции логики. Вместо того чтобы писать методы-расширения или утилитарные классы, вы помещаете логику прямо внутрь перечисления. В-третьих, для читаемости. Код становится самодокументируемым: каждое значение перечисления несёт в себе не только имя, но и контекст.

2. Установка

Библиотека устанавливается через NuGet. Выполните одну из следующих команд в консоли диспетчера пакетов или в терминале .NET CLI:

# Через .NET CLI
dotnet add package Ardalis.SmartEnum

# Через Package Manager Console
Install-Package Ardalis.SmartEnum

После установки добавьте using Ardalis.SmartEnum; в начало файла.

3. Быстрый старт — минимальный рабочий пример

Создадим простое перечисление для дней недели. Обратите внимание: мы наследуемся от SmartEnum<TEnum, TValue>, где TEnum — это наш класс, а TValue — тип значения (обычно int или string).

using Ardalis.SmartEnum;

public sealed class Weekday : SmartEnum<Weekday, int>
{
    // Статические поля — это и есть значения перечисления
    public static readonly Weekday Monday = new Weekday(nameof(Monday), 1);
    public static readonly Weekday Tuesday = new Weekday(nameof(Tuesday), 2);
    public static readonly Weekday Wednesday = new Weekday(nameof(Wednesday), 3);
    public static readonly Weekday Thursday = new Weekday(nameof(Thursday), 4);
    public static readonly Weekday Friday = new Weekday(nameof(Friday), 5);
    public static readonly Weekday Saturday = new Weekday(nameof(Saturday), 6);
    public static readonly Weekday Sunday = new Weekday(nameof(Sunday), 7);

    // Приватный конструктор обязателен
    private Weekday(string name, int value) : base(name, value) { }
}

// Использование
public class Program
{
    public static void Main()
    {
        var today = Weekday.Monday;
        Console.WriteLine(today.Name);   // Monday
        Console.WriteLine(today.Value);  // 1

        // Поиск по значению
        var dayFromValue = Weekday.FromValue(3);
        Console.WriteLine(dayFromValue.Name); // Wednesday

        // Поиск по имени
        var dayFromName = Weekday.FromName("Friday");
        Console.WriteLine(dayFromName.Value); // 5
    }
}

4. Основные методы/функции/классы

Рассмотрим ключевые возможности библиотеки. Все примеры будут строиться на следующем перечислении Status:

public sealed class Status : SmartEnum<Status, int>
{
    public static readonly Status New = new Status(nameof(New), 1, "Новый");
    public static readonly Status InProgress = new Status(nameof(InProgress), 2, "В работе");
    public static readonly Status Completed = new Status(nameof(Completed), 3, "Завершён");
    public static readonly Status Cancelled = new Status(nameof(Cancelled), 4, "Отменён");

    public string DisplayName { get; }

    private Status(string name, int value, string displayName) : base(name, value)
    {
        DisplayName = displayName;
    }
}

4.1. FromValue(TValue value)

Сигнатура: public static TEnum FromValue(TValue value)

Что делает: Возвращает экземпляр перечисления по его значению. Если значение не найдено, выбрасывает исключение SmartEnumNotFoundException.

var status = Status.FromValue(2);
Console.WriteLine(status.DisplayName); // В работе

4.2. TryFromValue(TValue value, out TEnum? result)

Сигнатура: public static bool TryFromValue(TValue value, out TEnum? result)

Что делает: Пытается найти экземпляр по значению. Возвращает true при успехе и false — если значение не найдено (без исключения).

if (Status.TryFromValue(5, out var unknownStatus))
{
    Console.WriteLine(unknownStatus.Name);
}
else
{
    Console.WriteLine("Статус не найден"); // Выведется это
}

4.3. FromName(string name)

Сигнатура: public static TEnum FromName(string name, bool ignoreCase = false)

Что делает: Возвращает экземпляр по имени (свойство Name). По умолчанию регистрозависимый. Параметр ignoreCase позволяет игнорировать регистр.

var status = Status.FromName("completed", ignoreCase: true);
Console.WriteLine(status.Value); // 3

4.4. TryFromName(string name, out TEnum? result)

Сигнатура: public static bool TryFromName(string name, out TEnum? result, bool ignoreCase = false)

Что делает: Аналог TryFromValue, но для поиска по имени. Безопасный способ без исключений.

if (Status.TryFromName("New", out var newStatus))
{
    Console.WriteLine(newStatus.DisplayName); // Новый
}

4.5. List (статическое свойство)

Сигнатура: public static IReadOnlyList<TEnum> List { get; }

Что делает: Возвращает неизменяемый список всех определённых значений перечисления.

foreach (var status in Status.List)
{
    Console.WriteLine($"{status.Name}: {status.DisplayName}");
}
// Вывод:
// New: Новый
// InProgress: В работе
// Completed: Завершён
// Cancelled: Отменён

4.6. Equals и == / !=

Сигнатура: public bool Equals(TEnum? other) (переопределён)

Что делает: Позволяет сравнивать экземпляры по значению (не по ссылке). Работает как с Equals, так и с операторами == и !=.

var status1 = Status.New;
var status2 = Status.FromValue(1);
Console.WriteLine(status1 == status2); // True
Console.WriteLine(status1.Equals(status2)); // True

4.7. CompareTo

Сигнатура: public int CompareTo(TEnum? other)

Что делает: Реализует интерфейс IComparable. Сравнивает по значению (Value). Позволяет сортировать перечисления.

var sorted = Status.List.OrderByDescending(s => s).ToList();
foreach (var s in sorted) Console.WriteLine(s.Name);
// Вывод: Cancelled, Completed, InProgress, New

4.8. ToString()

Сигнатура: public override string ToString()

Что делает: Возвращает имя перечисления (Name). Это удобно для логирования и отладки.

Console.WriteLine(Status.InProgress.ToString()); // InProgress

4.9. When (паттерн-матчинг)

Сигнатура: public TEnum When(TEnum value, Action action) (и перегрузки)

Что делает: Позволяет выполнить действие, если текущее значение равно указанному. Это альтернатива switch, но более читаемая.

var currentStatus = Status.New;
currentStatus
    .When(Status.New, () => Console.WriteLine("Создано"))
    .When(Status.InProgress, () => Console.WriteLine("В работе"))
    .When(Status.Completed, () => Console.WriteLine("Готово"))
    .Default(() => Console.WriteLine("Неизвестно"));

5. Реальный практический пример (100-200 строк кода)

Создадим систему обработки заказов с использованием SmartEnum. Заказ может иметь статус, и каждый статус определяет, какие действия над заказом возможны.

using Ardalis.SmartEnum;

// 1. Определяем перечисление статусов с дополнительной логикой
public sealed class OrderStatus : SmartEnum<OrderStatus, int>
{
    // Значения
    public static readonly OrderStatus Pending = new OrderStatus(nameof(Pending), 1, "Ожидает оплаты", false);
    public static readonly OrderStatus Paid = new OrderStatus(nameof(Paid), 2, "Оплачен", false);
    public static readonly OrderStatus Shipped = new OrderStatus(nameof(Shipped), 3, "Отправлен", false);
    public static readonly OrderStatus Delivered = new OrderStatus(nameof(Delivered), 4, "Доставлен", true);
    public static readonly OrderStatus Cancelled = new OrderStatus(nameof(Cancelled), 5, "Отменён", true);

    // Кастомные свойства
    public string DisplayName { get; }
    public bool IsFinal { get; } // Финальный ли статус (доставлен или отменён)

    private OrderStatus(string name, int value, string displayName, bool isFinal) : base(name, value)
    {
        DisplayName = displayName;
        IsFinal = isFinal;
    }

    // Метод, проверяющий возможность перехода
    public bool CanTransitionTo(OrderStatus newStatus)
    {
        if (this.IsFinal) return false; // Из финального статуса никуда нельзя
        if (newStatus == Cancelled) return true; // Отменить можно всегда (кроме финальных)
        if (this == Pending && newStatus == Paid) return true;
        if (this == Paid && newStatus == Shipped) return true;
        if (this == Shipped && newStatus == Delivered) return true;
        return false;
    }
}

// 2. Класс заказа
public class Order
{
    public int Id { get; }
    public string CustomerName { get; }
    public decimal Amount { get; }
    public OrderStatus Status { get; private set; }

    public Order(int id, string customerName, decimal amount)
    {
        Id = id;
        CustomerName = customerName;
        Amount = amount;
        Status = OrderStatus.Pending; // Начальный статус
    }

    public bool TryChangeStatus(OrderStatus newStatus)
    {
        if (!Status.CanTransitionTo(newStatus))
        {
            Console.WriteLine($"Ошибка: нельзя перейти из '{Status.DisplayName}' в '{newStatus.DisplayName}'");
            return false;
        }
        Status = newStatus;
        Console.WriteLine($"Заказ #{Id}: статус изменён на '{newStatus.DisplayName}'");
        return true;
    }

    public void PrintInfo()
    {
        Console.WriteLine($"Заказ #{Id} | Клиент: {CustomerName} | Сумма: {Amount:C} | Статус: {Status.DisplayName}");
    }
}

// 3. Использование
public class Program
{
    public static void Main()
    {
        var order = new Order(1, "Иван Иванов", 1500.50m);
        order.PrintInfo();

        // Попробуем оплатить
        order.TryChangeStatus(OrderStatus.Paid); // Успех

        // Попробуем отправить до оплаты (ошибка)
        order.TryChangeStatus(OrderStatus.Shipped); // Успех

        // Попробуем доставить
        order.TryChangeStatus(OrderStatus.Delivered); // Успех

        // Попробуем отменить после доставки (ошибка)
        order.TryChangeStatus(OrderStatus.Cancelled); // Ошибка: финальный статус

        // Выведем все возможные статусы
        Console.WriteLine("\nВсе статусы заказа:");
        foreach (var status in OrderStatus.List)
        {
            Console.WriteLine($"- {status.Name}: {status.DisplayName} (Финальный: {status.IsFinal})");
        }

        // Поиск по значению
        var statusFromValue = OrderStatus.FromValue(3);
        Console.WriteLine($"\nСтатус с value=3: {statusFromValue.DisplayName}");

        // Использование паттерн-матчинга
        var currentStatus = OrderStatus.Pending;
        currentStatus
            .When(OrderStatus.Pending, () => Console.WriteLine("Заказ ожидает оплаты"))
            .When(OrderStatus.Paid, () => Console.WriteLine("Заказ                    

Рекомендаций