« Все записи

Маленькие чудеса C#/.NET: забавы с методами enum

Продолжаю переводить "Маленькие чудеса...". После успешной трилогии - сериал. Оригинал статьи.

Еще раз позвольте окунуться в "Маленькие чудеса .NET" - эти маленькие "штучки" в языках платформы .NET и классах BCL, которые делают разработку проще за счет повышения читаемости кода, сопровождаемости или производительности.

Вероятно, каждый из нас использует перечисляемые типы время от времени в программах на C#. Перечисляемые типы, которые мы создаем - это отличный способ передать то, что значение может быть одним из набора дискретных значений (или сочетанием этих значений в случае битовых флагов).

Но возможности перечисляемых типов выходят далеко за рамки простого присваивания и сравнения, есть много методов в классе Enum (от которого "наследуются" все перечисления), которые могут дать вам еще больше возможностей при работе с ними.

IsDefined() - проверка, что данное значение присутствует в перечислении

Вы считываете значение для перечисления из источника данных, но не уверены, что это действительно допустимое значение? Приведение не скажет вам этого, и Parse() не гарантирует разбора, если вы передаете ему int или комбинацию флагов. Итак, что мы можем сделать?

Давайте предположим, что у нас есть небольшое перечисление, содержащее коды результатов, которые мы хотим возвращать из нашего слоя бизнес-логики:

public enum ResultCode
{
    Success,
    Warning,
    Error
}

В этом перечислении Success (Успешно) будет равен нулю (если другое значение не задано явно), Warning (Предупреждение) будет 1, а Error (Ошибка) будет 2.

Что же произойдет, если мы имеем такой код, где мы получаем код результата из другого источника данных (это может быть база данных, это может быть веб-сервис и т.д.)?

public ResultCode PerformAction()
{
    // вызываем какой-то метод, который возвращает int.
    int result = ResultCodeFromDataSource();

    // это удастся, даже если результат < 0 или > 2.
    return (ResultCode) result;
}

Что же произойдет, если результат равен -1 или 4? Приведение не вызовет ошибки, так что в итоге мы получим экземпляр ResultCode, который будет иметь значение за пределами границ констант перечисления, которые мы определили.

Это означает, что если у вас есть такой блок кода:

switch (result)
{
case ResultType.Success:
    // выполняется код для успешного результата
    break;

case ResultType.Warning:
    // выполняется код для предупреждения
    break;

case ResultType.Error:
    // выполняется код для ошибки
    break;
}

вы не войдете ни в один из этих блоков (что, между прочим, является хорошим аргументом в пользу того, чтобы всегда иметь блок default в операторе switch).

Так что же вы можете сделать? В классе Enum есть удобный статический метод IsDefined(), который вернет true, если значение перечисления определено.

public ResultCode PerformAction()
{
    int result = ResultCodeFromDataSource();

    if (!Enum.IsDefined(typeof(ResultCode), result))
    {
        throw new InvalidOperationException("Enum out of range.");
    }

    return (ResultCode) result;
}

Это часто рекомендуется делать после Parse() или приведения значения к enum, как способ обойти значения, которые не могут быть определены.

Если вам не нравится синтаксис передачи в типе перечисления, вы можете сделать его более прозрачным его путем создания метода расширения, который позволит вам вызывать IsDefined() для любого экземпляра enum:

public static class EnumExtensions
{
    // вспомогательный метод, который сообщит вам, что перечисляемое значение определено в данном перечислении    
    public static bool IsDefined(this Enum value)
    {
        return Enum.IsDefined(value.GetType(), value);
    }
}

HasFlag() - простой способ убедиться, что бит или биты установлены

Большинству из нас, кто вышел из мира C (си), множество раз приходилось иметь дело с битовыми флагами (bit flags). Таким образом, использование флагов может стать почти второй натурой (для быстрого повышения квалификации по битовым флагам в перечисляемых типах можно посмотреть один из моих старых постов здесь).

Тем не менее в языках высокого уровня, таких как C#, необходимость манипулировать отдельными битовыми флагами несколько уменьшились, и код для проверки значений битовых флагов перечисления может быть очевидным для продвинутых разработчиков, но загадочным для начинающих.

Для примера предположим, что у вас есть перечисление для платформы обмена сообщениями, которое содержит битовые флаги:

// Как правило, мы используем множественное число для наименований флаговых перечислений 
[Flags]
public enum MessagingOptions
{
    None = 0,
    Buffered = 0x01,
    Persistent = 0x02,
    Durable = 0x04,
    Broadcast = 0x08
}

Мы можем объединять эти битовые флаги, используя оператор "побитовое ИЛИ" (символ вертикальной черты "|"):

// комбинируем битовые флаги
var myMessenger = new Messenger(MessagingOptions.Buffered | MessagingOptions.Broadcast);

Теперь, если мы хотим проверить флаги, мы должны протестировать их, используя оператор "побитовое И" (символ "&"):

if ((options & MessagingOptions.Buffered) == MessagingOptions.Buffered)
{
    // выполнить код установки буферизации...
    // ...
}

Насколько "|" для комбинирования флагов лекго читается опытными разработчиками, настолько при тестировании флагов с помощью "&" начинающим разработчикам лекго ошибиться. Прежде всего вы должны выполнить AND для комбинации флагов со значением, а затем вы должны протестировать комбинацию флагов саму по себе (и не только для ненулевых значений)!

Это потому, что проверяемая комбинация флагов может объединять несколько битов, в этом случае, если только один бит установлен, результат будет ненулевым, но не обязательно содержать все желаемые биты!

Скажем спасибо фреймворку .NET 4.0 - он дал нам метод HasFlag(). Этот метод может быть вызван из экземпляра enum, чтобы убедиться, что флаг установлен, и избежать написания "мудреной" логики самостоятельно. Не говоря уже о том, что он будет более понятным для начинающих разработчиков:

if (options.HasFlag(MessagingOptions.Buffered))
{
    // выполнить код установки буферизации...
    // ...
}

Выглядит гораздо более кратко и однозначно, тем самым увеличивая сопровождаемость и читабельность.

Было бы неплохо иметь соответствующий метод SetFlag(), но, к сожалению, универсальные типы не позволяют специализировать Enum, что делает эту задачу немного сложнее. Это может быть сделано, но вы должны совершить некоторые преобразования в числовое значение и обратно в enum, что делает его менее выигрышным, чем метод HasFlag().

Но если вы хотите создать его для симметрии, он будет выглядеть примерно так:

public static T SetFlag<T>(this Enum value, T flags)
{
    if (!value.GetType().IsEquivalentTo(typeof(T)))
    {
        throw new ArgumentException("Enum value and flags types don't match.");  //Типы значения Enum и флагов не совпадают.
    }

    // да, это уродливо, но, к сожалению, мы должны использовать промежуточное приведение
    return (T)Enum.ToObject(typeof(T), Convert.ToUInt64(value) | Convert.ToUInt64(flags));
}

Заметим, что поскольку перечисляемые типы - это типы-значения (value types), мы должны обязательно присвоить результат чему-то (как string.Trim()). Кроме того, вы можете вызывать последовательно несколько операций SetFlag(), или, если хотите, создать такой метод, который принимает переменный список аргументов.

Parse() и ToString() - переход от строки к перечислению и обратно

Иногда вам может потребоваться получить перечисляемое значение из строки или преобразовать его в строку - Enum имеет встроенные методы, которые позволяют это сделать. Многие знают о них, но не представляют, как много возможностей у этих двух методов.

Например, если вы хотите преобразовать строку в перечисление, то это легко сделать, и это работает так же, как можно было ожидать от числовых типов:

string optionsString = "Persistent";

// можно использовать Enum.Parse, который вызывает исключение, если встретит что-то, что ему не понравится...
var result = (MessagingOptions)Enum.Parse(typeof(MessagingOptions), optionsString);

if (result == MessagingOptions.Persistent)
{
    Console.WriteLine("It worked!"); //Это работает!
}

Обратите внимание, что метод Enum.Parse() вызовет исключение, если он встретит значение, которое ему не нравится. Но значения, которые нравятся, довольно гибкие! Вы можете передать одно значение или список разделенных запятыми значений для флагов, и он разберет их все и установит все биты:

// для строковых значений можно иметь одно или несколько через запятую.
string optionsString = "Persistent, Buffered";

var result = (MessagingOptions)Enum.Parse(typeof (MessagingOptions), optionsString);

if (result.HasFlag(MessagingOptions.Persistent) && result.HasFlag(MessagingOptions.Buffered))
{
    Console.WriteLine("It worked!"); //Это работает!
}

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

// 3 представляет собой сочетание Buffered (0x01) и Persistent (0x02)
var optionsString = "3";

var result = (MessagingOptions) Enum.Parse(typeof (MessagingOptions), optionsString);

if (result.HasFlag(MessagingOptions.Persistent) && result.HasFlag(MessagingOptions.Buffered))
{
    Console.WriteLine("It worked again!"); //Это работает опять!
}

А если вы не уверены, что разбор будет работать, и не хотите обрабатывать исключение, вы можете использовать TryParse():

string optionsString = "Persistent, Buffered";
MessagingOptions result;

// Попытка разбора возвращает true в случае успеха, и результат в параметре out 
if (Enum.TryParse(optionsString, out result))
{
    if (result.HasFlag(MessagingOptions.Persistent) && result.HasFlag(MessagingOptions.Buffered))
    {
        Console.WriteLine("It worked!");
    }
}

Мы рассмотрели преобразование строки в перечисление, как насчет обратного: преобразования перечисления в строку? ToString() является очевидным и основным выбором для большинства из нас, но знаете ли вы, что вы можете передать форматную строку для перечисляемых типов, которая укажет, как они должны выводиться в виде строки?:

MessagingOptions value = MessagingOptions.Buffered | MessagingOptions.Persistent;

// общий формат, который используется по умолчанию,
Console.WriteLine("Default    : " + value);
Console.WriteLine("G (default): " + value.ToString("G"));

// формат флагов, даже если тип не имеет атрибут Flags.
Console.WriteLine("F (flags)  : " + value.ToString("F"));

// целочисленный формат, значение как число.
Console.WriteLine("D (num)    : " + value.ToString("D"));

// шестнадцатеричный формат, значение как шестнадцатеричное число
Console.WriteLine("X (hex)    : " + value.ToString("X"));

Будет выведено:

Default    : Buffered, Persistent
G (default): Buffered, Persistent
F (flags)  : Buffered, Persistent
D (num)    : 3
X (hex)    : 00000003

Здесь вы не можете реально увидеть разницу между G и F, потому что я использовал [Flags] enum, разница в том, что "F" рассматривает перечисление, как будто это флаги, даже если атрибут [Flags] отсутствует. Давайте используем перечисление без флагов, например наше ранее определенное ResultCode:

// да, мы можем сделать это, даже если это не [Flags] enum.
ResultCode value = ResultCode.Warning | ResultCode.Error;

И если мы выполним наш код с теми же форматами, мы получим:

Default    : 3
G (default): 3
F (flags)  : Warning, Error
D (num)    : 3
X (hex)    : 00000003

Обратите внимание, что поскольку у нас было несколько значений скомбинированных вместе, но это не было перечислением отмеченным [Flags], G и формат по умолчанию возвратил нам число вместо значения. Это потому, что это значение не являлось допустимым для перечисления с одним значением. Однако, используя форматную строку F, мы получили флаги, хотя enum и не был отмечен аттрибутом [Flags].

Если вы хотите получить перечисление, отображаемое соотвествующим образом в зависимости от аатрибута [Flags], используйте форматныю строку G, которая используется по умолчанию. Если вы хотите всегда видеть флаги, используйте F. Для цифрового вывода, очевидно, D или X являются лучшим выбором в зависимости от того, требуется десятичная или шестнадцатеричная форма.

Резюме

Надеюсь, сегодня вы увидели пару новых трюков с использованием класса Enum! Я добавлю еще маленьких чудес, как только вспомню о них, и спасибо всем за неоценимый вклад!

Progg it

comments powered by Disqus