C#: Расширенная обработка элементов перечислений (enum) с помощю атрибутов

2010-11-09 papirosnik C#

Платформа .NET вообще и язык C# в частности привносят в область традиционного ООП  новый инструмент или даже парадигму: атрибуты. Основной задачей, стоящей перед атрибутами, является полная (истинная) инкапсуляция. Благодаря атрибутам можно хранить полную (всё же я предпочитаю более осторожный вариант: дополнительную) информацию о классах, их членах, методах и т.п. Атрибут может иметь практически любая сущность в программе на C#. Атрибуты позволяют избежать использования "разрывных решений" , когда в дополнение к файлам с исходным кодом необходимо иметь ещё файлы с дополнительной информацией (IDL, DEF и т.п.)
 Касательно темы статьи: нас интересует, как при помощи атрибутов наделить стандартное перечисление (System.Enum) новыми свойствами.

Сразу необходимо отметить, что разработчики .NET Framework уже позаботились о нас: начиная с версии 2.0 .NET имеет класс DescriptionAttribute в пространстве имён System.ComponentModel. Чтобы использовать этот класс в своей программе без дополнительных квалификаторов, достаточно в самом начале в секции using добавить using System.ComponentModel;

Так что же он нам даёт? Этот класс является наследником System.Attribute и позволяет любую сущность в программе наделять описанием. На примере Enum мы будем использовать описание (Description) для каждого члена перечисления.
Предположим, имеем такой код:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace EnumSample
{
    class Program
    {
        private enum ItemCounter
        {
            First,
            Second,
            Third
        }
 
        static void Main(string[] args)
        {
            Console.WriteLine(ItemCounter.First);
            Console.WriteLine(ItemCounter.Second);
            Console.WriteLine(ItemCounter.Third);
            Console.ReadKey();
        }
    }
}

Запустив эту программу на выполнение, мы получим следующий вывод:

First
Second
Third

Такое представление численного типа (Enum по умолчанию основан на типе Int32) возможно благодаря тому, что в C# каждая сущность имеет встроенный метод ToString() для её представления в строковом виде. В случае интегральных типов (например Int32) всё работает прекрасно: ToString() переводит число в строку. В случае же классов, структур, перечислений дела обстоят так, что компилятор затрудняется в принятии решения насчёт того, как сущность такого типа представить в виде строки. Не мудрствуя лукаво, метод ToString() в данном случае просто возвращает имя объекта класса (или самого класса в некоторых случаях, например статических).

Обратно возвращаясь к нашему перечислению (ItemCounter в данном конкретном случае, но System.Enum в общем) можно отметить, что ToString() (который неявно вызывается при Console.WriteLine) возвращает имя члена перечисления. Оставим вопрос, откуда среда времени выполнения (Runtime Environment) знает это имя на этапе выполнения программы (тем, кто пришёл из с++ этот момент может показаться удивительным), вскользь лишь заметим, что это возможно благодаря такому замечательному свойству C# как отражение (Reflection). Мы ещё к нему вернёмся немного позже, а пока с прискорбием отмечаем второй факт, что посколькуу System.Enum — это специальный, вырожденный вид класса, который не может иметь функций-членов, то нельзя перегрузить метод ToString() для перечисления.
 Можно задуматься, а надо ли? Надо. Практически во всех программах используется тем или иным способом трактовка членов перечисления. Например, есть перечисления, содержащее все возможные состояния объекта (0 — хорошо, 1 — файл не найден, 2 — отказано в доступе и т.п.) и нам надо это состояние отображать в строке статуса. Для этого обычно используется какая-то функция, которая на входе получает значение перечисления, на выходе возвращает строку, содержащую развёрнутого описание. Парадигма атрибутов C# предлагает решение, более близко соответствующее концепции ООП, и позволяющее хранить эти описания непосредственно в самом перечислении. Таким образом, перечисление становится самодостаточным. Оно не требует дополнительных внешних функций и данных. Перепишем наш гипотетический пример следующим образом:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
 
namespace EnumSample
{
	class Program
	{
 
		private enum ItemCounter
		{
			[Description("Первый элемент")]         First,
			[Description("Второй элемент")]         Second,
			[Description("Третий соответственно")]  Third
		}
 
		static void Main(string[] args)
		{
			Console.WriteLine(ItemCounter.First);
			Console.WriteLine(ItemCounter.Second);
			Console.WriteLine(ItemCounter.Third);
			Console.ReadKey();
		}
	}
}

Вывод не поменялся, но мы уже немного продвинулись вперёд. Каждый элемент перечисления теперь имеет своё описание. Описание имеет Тип DescriptionAttribute, но, благодаря принятому в C# соглашению об именовании специальных типов, суффикс Attribute в данном случае можно опустить. Кроме того, мы добавили вверху строчку using System.ComponentModel; которая сообщает о том, где компилятору искать описание System.DescriptionAttribute.

Тут нужно вовремя признаться, что я немного покривил душой, выдавая атрибуты как средство, не требующее внешних функций. Нам всё-таки одна потребуется, но она будет применяться для всех возможных перечислений в программе. Согласитесь, не велика жертва. Если использовать принцип, применяемый в с++ программах, то есть описание во внешних файлах или функциях, то пришлось бы иметь такие функции и/или файлы для каждого перечисления.

Ещё один момент, о котором я малодушно сразу умолчал — это локализация. Если мы захотим хранить описания на разных языках, то строки описания всё-таки придётся хранить в каких-то внешних файлах. Но и в этом случае атрибуты нам здорово помогут, позволяя хранить ключи для локализованных значений. Итак, поскольку функцию ToString() перекрывать нельзя (плюс она также используется для некоторых внутренних нужд, например для Parse()), то заведём новую, назвав её например GetDescription(). Объявлять её надо в каком-нибудь публичном статичном классе — класс Program из нашего примера очень даже подходит для этих целей.

1
2
3
4
5
6
7
8
public static string GetDescription(this Enum value)
{
	DescriptionAttribute[] da =
               (DescriptionAttribute[])(value.GetType().
                      GetField(value.ToString()).
                             GetCustomAttributes(typeof(DescriptionAttribute),false));
	return da.Length > 0 ? da[0].Description : value.ToString();
}

Не так страшен чёрт, как его тут расписали :). Здесь мы просто воспользовались ранее упоминавшимся отражением (Reflection) для того, чтобы получить у перечисления все атрибуты класса DescriptionAttribute. Если у члена перечисления есть описание (то есть имеется атрибут Description), то метод GetDescription() вернёт его, иначе же он вернёт простое строковое представление объекта по умолчанию (задействовав для этого ToString()). Теперь допишем код нашего примера расширенным выводом, после чего целиком он будет выглядеть так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
 
namespace EnumSample
{
	class Program
	{
		public static string GetDescription(this Enum value)
		{
			DescriptionAttribute[] da= (DescriptionAttribute[])
			(value.GetType().GetField(value.ToString()).
			GetCustomAttributes(typeof(DescriptionAttribute),false));
			return da.Length>0 ? da[0].Description:value.ToString();
		}
 
		private enum ItemCounter
		{
			[Description("Первый элемент")]          First,
			[Description("Второй элемент")]          Second,
			[Description("Третий соответственно")]   Third
		}
 
		static void Main(string[] args)
		{
			Console.WriteLine(ItemCounter.First.GetDescrption());
			Console.WriteLine(ItemCounter.Second.GetDescrption());
			Console.WriteLine(ItemCounter.Third.GetDescrption());
			Console.ReadKey();
		}
	}
}

Оговоримся, что этот код использует некоторые допуски и соглашения: например, если у члена перечисления есть несколько описаний, он вернёт самое первое. Взглянув на вывод, увидим, что теперь вместо сухих идентификаторов выводится человеко-читаемая строка, которую можно использовать в различных сообщениях пользователю:

Первый элемент
Второй элемент
Третий соответственно

Теперь можно использовать в программе сколько угодно перечислений и вызывать для их членов функцию GetDescription(). Если только член перечисления имеет описание (ведь его же можно и свободно опускать), то функция GetDescription() вернёт его в неизменном виде. В случае, когда расширенное описание нам не нужно, эта функция ведёт себя аналогично ToString(). Эта возможность использовать одну функцию для многих перечислений существует благодаря тому, что:

  • Все перечисления порождены от System.Enum
  • Функция GetDescription() описана в статическом классе
  • В описании параметра для функции GetDescription() используется ключевое слово this, означающее, что эта функция является внешней по отношению к члену перечисления, но выглядеть в тексте будет так, как будто принадлежит ему.

attributes, DescriptionAttribute, enum, Reflection, пользовательские атрибуты,

2 комментария to “C#: Расширенная обработка элементов перечислений (enum) с помощю атрибутов”


Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Powered by WordPress. Designed by elogi.