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("Заказ