Перечислимые типы и битовые флаги
Перечислимые типы
Перечислимым (enumerated type) называют тип, в котором описан набор пар, состоящий из символьных имён и значений. Например:
internal enum Color
{
White, // Присваивается значение 0
Red, // Присваивается значение 1
Green, // Присваивается значение 2
Blue, // Присваивается значение 3
Orange // Присваивается значение 4
}
Можно явно написать в коде числовые значения, однако у перечислений есть два преимущества:
- Программу с использованием перечислений проще читать, писать и сопровождать.
- Перечислимые типы подвергаются строгой проверке типов.
В CLR перечислимые типы не просто идентификаторы, но они играют важную роль в системе типов. Каждый перечислимый тип напрямую наследуется от типа System.Enum, производного от System.ValueType, который наследуется от System.Object. Перечислимые типы относятся к значимым типам и могут быть как в неупакованной, так и упакованной формах. Однако в отличие от других значимых типов у перечисления не может быть методов (а также свойств и событий). При компиляции перечислимого типа каждый идентификатор превращается в константное поле типа:
internal struct Color : System.Enum
{
// Далее перечислены открытые константы, определяющие символьные имена и значения
public const Color White = (Color) 0;
public const Color Red = (Color) 1;
public const Color Green = (Color) 2;
public const Color Blue = (Color) 3;
public const Color Orange = (Color) 4;
// Далее находится открытое поле экземпляра со значением переменной Color
// Код с прямой ссылкой на этот экземпляр невозможен
public Int32 value__;
}
Перечислимый тип - обычная структура с набором константных полей и одним экземплярным полем. Константные поля попадают в метаданные сборки, откуда их можно извлечь через рефлексию. То есть в период выполнения можно получить все идентификаторы и их значения, а также преобразовать строковый идентификатор в числовое значение. Однако все те же операции предоставлены базовом типом System.Enum, который предлагает статические и экземплярные методы, выполняющие специальные операции над экземплярами перечислимых типов.
Описанные перечислимым типов символы являются константами. Встречая в коде символическое имя перечислимого типа, компилятор заменяет его на числовое значение. То есть сама сборка с перечислимым типом может даже не понадобиться, если в коде не будет ссылок на сам перечислимый тип, а не на его значения. Однако именно это порождает проблему версий, связанная с необходимостью перекомпиляции исходного кода при изменениях в сборке с перечислимым типом.
Для System.Enum существует статический метод GetUnderlyingType(), а для System.Type - экземплярный метод GetEnumUnderlyingType(). Оба этих метода возвращают базовый тип, используемый для хранения значений перечислимого типа. В основе любого перечисления лежит один из целочисленных примитивных типов. Все эти примитивные типы имеют аналоги в FCL, однако компилятор пропускает только примитивный тип.
Компилятор считает перечисления примитивным типам, поэтому к ним можно применяться стандартные операторы. Операторы применяются к полю value__ экземпляра перечисления, а компилятор C# допускает приведение экземпляров одного перечислимого типа к другому. Также поддерживается явное и неявное приведение.
Имеющийся экземпляр перечислимого типа можно связать со строковым представлением, через вызов ToString() (возможно применение форматирования), унаследованный от System.Enum. Помимо ToString() для перечисления доступен статический метод Format(), служащий для форматирования значений перечислимого типа. В общем случае ToString() удобнее, но Format() статический, что позволяет вызывать его без наличия экземпляра перечислимого типа.
Можно объявить перечисление, где различные идентификаторы соответствуют одинаковым числовым значениям. В процессе преобразования числа в символьное значение методы форматирования вернут один из символов, правда, неизвестно какой. Если соответствие не найдено, то вернётся числовое значение.
Статический метод System.Enum.GetValues() и метод GetEnumValues() экземпляра System.Type создают массив, элементами которого являются символьные имена перечислений. И каждый элемент содержит соответствующее числовое значение. Помимо этого, существует обобщённый метод GetEnumValues<T>(), который улучшает типобезопасность и возвращает массив перечислимого типа, благодаря чему отпадает необходимость в явном приведении, так как необобщённый метод возвращает System.Array.
Чаще всего символьные имена перечислимых типов отображаются с помощью метода ToString() с использованием общего формата (если выводимые строки не требуют локализации, которую не поддерживают перечислимые типы).
Помимо рассмотренных ранее методов существуют ещё некоторые:
- Методы для получения строкового представления числового значения:
public static String GetName(Type enumType, Object value);- Определен в System.Enumpublic String GetEnumName(Object value);- Определен в System.Type
- Методы для получения массива строк (по одной на каждое символьное имя из перечисления):
public static String[] GetNames(Type enumType);- Определен в System.Enumpublic String[] GetEnumNames();- Определен в System.Type
- Методы для определения значения, соответствующего символьному идентификатору (все определены в
System.Enum):public static Object Parse(Type enumType, String value);public static Object Parse(Type enumType, String value, Boolean ignoreCase);public static Boolean TryParse<TEnum>(String value, out TEnum result) where TEnum: struct;public static Boolean TryParse<TEnum>(String value, Boolean ignoreCase, out TEnum result) where TEnum : struct;
- Методы для допустимости числового значения для данного перечислимого типа (в случае недопустимости стоит выбрасывать
ArgumentOutOfRangeException):public static Boolean IsDefined(Type enumType, Object value);- Определен в System.Enumpublic Boolean IsEnumDefined(Object value);- Определен в System.Type
Метод IsDefined() стоит использовать с осторожностью. Во-первых, он выполняет поиск с учётом регистра. Во-вторых, работает крайне медленно, так как использует отражение. Для проверки допустимости значения для перечислимого типа стоит определить собственный метод. Кроме того, метод работает только для перечислений, определённых в той же сборке, из которой вызывается.
Наконец, стоит упомянуть несколько статических методов, преобразующих целочисленные примитивные типы в экземпляры примитивного типа.
Перечислимые типы всегда применяют в сочетании с другим типом, например, в качестве параметров методов или возвращаемых значений методов, свойств или полей. В FCL перечислимые типы часто определяются на уровне класса, которым используются, чтобы их было проще найти, поэтому, при отсутствии конфликта имён лучше делать именно так.
Битовые флаги
В FCL есть специальный тип FileAttributes, где каждый разряд соответствует какому-то атрибуту файла. Данный тип описан следующим образом:
[Flags, Serializable]
public enum FileAttributes
{
ReadOnly = 0x0001,
Hidden = 0x0002,
System = 0x0004,
Directory = 0x0010,
Archive = 0x0020,
Device = 0x0040,
Normal = 0x0080,
Temporary = 0x0100,
SparseFile = 0x0200,
ReparsePoint = 0x0400,
Compressed = 0x0800,
Offline = 0x1000,
NotContentIndexed = 0x2000,
Encrypted = 0x4000
}
В классе Enum имеется метод HasFlag(), определяемый следующим образом: public Boolean HasFlags(Enum flag), который и определяет наличие флага. Однако использовать его не стоит, потому что он принимает значения типа Enum, а значит, произойдёт упаковка.
При создании набора комбинируемых друг с другом битовых флагов используют перечисления. Однако несмотря на внешнюю схожесть, перечислимые типы семантически отличаются от битовых флагов. Если в первом случае имеются отдельные числовые значения, то во втором - набор флагов, одни из которых могут быть установлены, а другие нет.
Определяя битовые флаги, каждому идентификатору стоит явно присвоить числовое значение. Обычно каждому идентификатору соответствует лишь один бит. Часто приходится видеть идентификатор None, значение которого равно 0. Ещё можно определить идентификаторы, представляющие часто используемые комбинации. К каждому перечислимому типу, используемому в качестве флагов, настоятельно рекомендуется применять специализированный атрибут.
Для работы с битовыми флагами можно использовать все методы, которые применимы к обычным перечислениям. Однако некоторые из них стоит переопределить (например, ToString(), чтобы он возвращал не числовое представление, для которого не сможет найти символьного идентификатора, а их набор идентификаторов для отдельных битовых флагов). Если метод ToString() находит у перечислимого типа атрибут [Flags], то он действует по следующему алгоритму:
- Получает набор числовых значений, определённых в перечислении, и сортирует их в нисходящем порядке.
- Для каждого значения выполняется операция конъюнкции с экземпляром перечисления. Если результат равен числовому значению, связанная с ним строка добавляется в итоговую строку, соответствующие биты считаются учтёнными и сбрасываются. Операция повторяется до завершения проверки или до сброса всех битов.
- Если после проверки есть несброшенные биты, значит им нельзя сопоставить идентификаторы. Тогда возвращается исходное число.
- Если исходное значение экземпляра не равно нулю, метод возвращает набор символов, разделённых запятой.
- Если в исходном значении был ноль, а в перечислении был такой идентификатор, то он возвращается.
- Если алгоритм доходит до данного шага, то возвращает 0.
Если перечисление не помечено атрибутом, то похожий результат можно получить, применив к перечислению метод .ToString("F"). Если числовое значение содержит бит, для которого нет идентификатора, тогда вернётся десятичное число.
Идентификаторы могут быть не только степенью двойки, но также шестнадцатеричным значением.
Можно не только получить символьные представления битовых флагов по числу, но и наоборот, распарсить строку в битовые флаги. Для этого можно воспользоваться статическими методами Enum.Parse() или Enum.TryParse<T>(). При их вызове происходит следующее:
- Удаляются все проблемы из начала и конца строки.
- Если первым символом является цифра, знаки "+" или "-", тогда строка считается числом и возвращается соответствующий экземпляр перечисления.
- Переданная строка разбивается на разделённые запятыми лексемы, и у каждой лексемы удаляются все пробелы в начале и конце.
- Выполняется поиск каждой лексемы среди символьных значений. Если лексема найдена, тогда её числовое значение путём дизъюнкции присоединяется к результату, иначе возвращается
ArgumentExceptionилиfalseв зависимости от метода. Метод переходит к анализу следующей лексемы. - После обнаружения и проверки всех лексем возвращается результат.
Для битовых флагов нельзя применять метод IsDefined() по следующим причинам:
- Метод не разбивает строку на лексемы, а ищет её целиком, соответственно, может никогда не найти результат, если в строке несколько лексем.
- После передачи числового значения метод ищет всего лишь один символьный идентификатор. А для битовых флагов вероятность найти то же число крайне мала, и обычно возвращается
false.
Добавление методов к перечислимым типам
Методы нельзя поместить внутрь перечислимых типов, но можно создать методы расширения, которые будут вести себя подобно экземплярным методам. Особенно это актуально для битовых флагов.
