Avatar ·

Как преобразовать строку в enum в C#? (Enum.Parse, TryParse, игнорирование регистра)

Всем привет! Я новичок в C# и застрял на, казалось бы, простой задаче. У меня есть перечисление (enum) для статусов заказа: ```csharp public enum OrderStatus { Pending, Processing, Shipped, Delivered, Cancelled } ``` Пользователь вводит статус в текстовое поле (например, "Shipped" или "shipped"). Мне нужно преобразовать эту строку в значение `OrderStatus`, чтобы использовать его в логике программы. Я пробовал просто присвоить строку переменной типа `OrderStatus`: ```csharp string userInput = "Shipped"; OrderStatus status = userInput; // Ошибка компиляции: Cannot implicitly convert type 'string' to 'OrderStatus' ``` Естественно, это не работает. Я знаю, что есть метод `Enum.Parse`, но читал, что он бросает исключение, если строки нет в enum. А что если пользователь введет "shipped" с маленькой буквы или опечатается? Как правильно и безопасно преобразовать строку в enum, учитывая возможные ошибки ввода? Желательно с примерами кода. Заранее спасибо за помощь! <<>>

Основное решение: Enum.Parse с обработкой исключений

Самый простой способ — использовать статический метод Enum.Parse(). Он преобразует строку в значение перечисления. Но, как вы правильно заметили, если строка не соответствует ни одному из имен элементов enum, будет выброшено исключение ArgumentException. Поэтому его нужно оборачивать в try-catch.

string userInput = "Shipped";
try
{
    OrderStatus status = (OrderStatus)Enum.Parse(typeof(OrderStatus), userInput);
    Console.WriteLine($"Статус: {status}");
}
catch (ArgumentException)
{
    Console.WriteLine("Ошибка: введено некорректное значение статуса.");
}

Лучшая практика: Enum.TryParse (без исключений)

Начиная с .NET 4.0, появился метод Enum.TryParse(). Он не бросает исключение, а возвращает bool — успешно ли прошло преобразование. Это гораздо безопаснее и производительнее, так как исключения в C# дорогие.

string userInput = "Shipped";
if (Enum.TryParse(userInput, out OrderStatus status))
{
    Console.WriteLine($"Статус: {status}");
}
else
{
    Console.WriteLine("Ошибка: введено некорректное значение статуса.");
}

Игнорирование регистра (case-insensitive)

По умолчанию оба метода чувствительны к регистру. Если пользователь введет "shipped" (с маленькой буквы), преобразование не сработает. Чтобы это исправить, используйте параметр ignoreCase: true:

string userInput = "shipped"; // с маленькой буквы

// Вариант с TryParse с игнорированием регистра
if (Enum.TryParse(userInput, ignoreCase: true, out OrderStatus status))
{
    Console.WriteLine($"Статус: {status}"); // Выведет: Shipped
}

// Вариант с Parse с игнорированием регистра
OrderStatus status2 = (OrderStatus)Enum.Parse(typeof(OrderStatus), userInput, ignoreCase: true);

Обработка числовых значений

Интересный момент: Enum.TryParse и Enum.Parse также могут преобразовывать строки, содержащие числа (например, "2" для Shipped, если индексы идут с 0). Это может быть как плюсом, так и источником ошибок. Если вы хотите разрешить ТОЛЬКО имена, а не числа, нужно добавить дополнительную проверку:

string userInput = "2";
if (Enum.TryParse(userInput, out OrderStatus status) && Enum.IsDefined(typeof(OrderStatus), status))
{
    Console.WriteLine($"Статус: {status}"); // Сработает, если "2" — это Shipped
}
else
{
    Console.WriteLine("Некорректное значение.");
}

Без проверки Enum.IsDefined даже строка "99" будет преобразована в значение 99, хотя такого элемента в enum нет. Это может привести к неожиданным багам.

Обобщенный метод-помощник (Generic helper)

Для удобства можно написать универсальный метод:

public static T ParseEnum<T>(string value, bool ignoreCase = true) where T : struct, Enum
{
    if (Enum.TryParse(value, ignoreCase, out T result) && Enum.IsDefined(typeof(T), result))
    {
        return result;
    }
    throw new ArgumentException($"'{value}' не является допустимым значением для {typeof(T).Name}");
}

// Использование:
OrderStatus status = ParseEnum<OrderStatus>("shipped");

Итоговые рекомендации

  • Всегда используйте Enum.TryParse — он безопаснее и быстрее, чем Enum.Parse с try-catch.
  • Включайте ignoreCase: true, если ваш ввод может быть в любом регистре (почти всегда так).
  • Проверяйте через Enum.IsDefined, если вы не хотите разрешать числовые строки.
  • Для часто используемых преобразований вынесите логику в generic-метод.
<<>>

Альтернативный подход: Использование атрибутов Description или пользовательских словарей

Иногда строки, которые вы получаете (из базы данных, API, пользовательского ввода), не совпадают с именами элементов enum. Например, в enum статус называется InProgress, а в базе данных хранится "В обработке" на русском. В таких случаях Enum.Parse не поможет.

Решение — использовать атрибут Description из пространства имен System.ComponentModel:

public enum OrderStatus
{
    [Description("Ожидание")]
    Pending,
    [Description("В обработке")]
    Processing,
    [Description("Отправлено")]
    Shipped
}

// Метод для получения enum по Description
public static T GetEnumFromDescription<T>(string description) where T : struct, Enum
{
    foreach (var field in typeof(T).GetFields())
    {
        var attribute = field.GetCustomAttribute<DescriptionAttribute>();
        if (attribute != null && attribute.Description == description)
        {
            return (T)field.GetValue(null);
        }
    }
    throw new ArgumentException($"Описание '{description}' не найдено в {typeof(T).Name}");
}

// Использование:
OrderStatus status = GetEnumFromDescription<OrderStatus>("В обработке"); // Processing

Если атрибуты кажутся избыточными, можно просто создать статический словарь:

private static readonly Dictionary<string, OrderStatus> StatusMap = new(StringComparer.OrdinalIgnoreCase)
{
    ["ожидание"] = OrderStatus.Pending,
    ["в обработке"] = OrderStatus.Processing,
    ["отправлено"] = OrderStatus.Shipped
};

public static OrderStatus ParseFromRussian(string russianStatus)
{
    if (StatusMap.TryGetValue(russianStatus, out var status))
        return status;
    throw new ArgumentException($"Неизвестный статус: {russianStatus}");
}

Этот подход дает полный контроль над маппингом и удобен для локализации.