
Объектно-ориентированное программирование.-6
.pdf
§ 5.4. Атрибуты
Большинство языков программирования разрабатываются с учетом минимального набора необходимых возможностей. Например, мы уже изучили, какие модификаторы могут иметь классы, интерфейсы, методы, делегаты и т.д. Между тем, если ограничиться только ими, то можно отметить два недостатка:
1)Невозможно предвидеть все усовершенствования, которые могут потребоваться в будущем, и как они повлияют на способы выражения типов на данном языке;
2)Так как компиляторы распознают только предопределенные ключевые слова, отсутствует возможность создавать свои собственные.
Скажем, как создать связь между классом, написанным на языке C++, и URL-ссылкой на документацию для данного класса? Или как мы будете ассоциировать члены классов полями XML-файла или системой справки? Поскольку язык C++ разрабатывался задолго до прихода в нашу жизнь URL и XML, обе эти задачи выполнить довольно трудно.
До сих пор решения подобных проблем предполагают хранение дополнительной информации в отдельном файле (DEF, IDL и т.д.), которая затем связывается с тем или иным типом или членом. Главная проблема в том, что класс больше не является «самоописывающимся», т.е. теперь пользователь не может сказать о классе все, лишь взглянув на его определение.
Язык C# предлагает иную парадигму – атрибуты (attributes). Атрибуты предоставляют универсальные средства связи данных с типами. Поскольку мы можем создавать атрибуты на основе любой информации, существует стандартный механизм определения самих атрибутов и запроса членов или типов в период выполнения как связанных с ними атрибутов.
5.4.1. Синтаксис описания атрибутов
Синтаксис описания атрибутов следующий:
<атрибуты> :: "[" [<целевой объект> :] <атрибут1>, [<атрибут2>, ...] "]"
<целевой объект> :: assembly <целевой объект> :: event <целевой объект> :: field <целевой объект> :: method <целевой объект> :: module
401

<целевой объект> :: param <целевой объект> :: property <целевой объект> :: return <целевой объект> :: type
<атрибут> :: <тип атрибута> [(<аргументы>)]
<аргументы> :: <позиционные аргументы> [, <именованные аргументы>] <аргументы> :: <именованные аргументы>
<позиционные аргументы> :: <список фактических аргументов> <именованные аргументы> :: <идентификатор> = <выражение> [, ...]
Целевой объект указывается, когда контекст применения атрибута неоднозначен. Например:
[SomeAttribute] public int F (void);
Здесь атрибут может относиться как к методу, так и к возвращаемому им значению. В этом случае компилятор выбирает наиболее употребительный вариант:
1.Глобальный атрибут может применяться либо к сборке, либо к модулю. Для этого контекста нет значения по умолчанию, поэтому обязательно требуется указать целевой объект assembly или module соответственно;
2.Атрибут делегата может применяться либо к объявляемому делегату (по умолчанию или при указании type), либо к его возвращаемому значению
(return);
3.Атрибут метода может применяться либо к объявляемому методу (по умолчанию или при указании method), либо к его возвращаемому значению (return). Здесь под методами также подразумеваются операторы и методы доступа get при объявлении свойства или индексатора;
4.Атрибут события может применяться к объявляемому событию (по умолчанию или указанию event), к связанному полю (field) или к связанным методам add и remove (method). Если событие имеет методы доступа, допустимо только указание event;
5.Атрибут метода доступа set при объявлении свойства или индексатора, а также атрибут метода доступа add и remove при объявлении события может применяться либо к связанному методу (по умолчанию или при указании method), либо к его одиночному неявному параметру value (param), либо к возвращаемому значению (return).
В других контекстах описание целевого объекта разрешено, но излишне, т.к. доступен всего один объект, и он используется по умолчанию:
402

6.При объявлении перечислений, структур, классов и интерфейсов можно или включать описатель type, или опускать его;
7.При объявлении констант и полей можно или включать описатель field, или опускать его;
8.При объявлении конструкторов и деструкторов можно или включать описатель method, или опускать его;
9.При объявлении свойства или индексатора можно или включать описатель property, или опускать его;
10.При описании формального параметра можно или включать описатель param, или опускать его.
Задание недопустимого целевого объекта, например, param в объявлении класса, влечет предупреждение от компилятора. Такой атрибут будет проигнорирован. Использование атрибута с недопустимым объектом (например, при описании пространства имен) влечет ошибку компиляции.
Тип атрибута – имя класса, являющегося потомком System.Attribute. В принципе, здесь происходит создание экземпляра указанного класса. Позиционные аргументы – это фактические параметры конструктора, а именованные аргументы – список инициализации свойств класса. С аргументами мы разберемся позже, а пока опишем свой класс-потомок Attribute (базовый класс Attribute использовать нельзя, т.к. он абстрактный), и рассмотрим варианты его применения:
class MyAttr : Attribute {}
[module: MyAttribyte] public delegate int Del1(int i); // Предупреждение [type: MyAttr] public delegate int Del2(int i);
[return: MyAttr] public delegate int Del3(int i);
[type: MyAttr] enum MyEnum { } [type: MyAttr] struct MyStruct { };
[type: MyAttr] interface IMyInterface { }
[type: MyAttr] class MyClass
{
[field: MyAttr] public const int X = 5; [field: MyAttr] public int Y = 5;
[method: MyAttr] public MyClass() { } [method: MyAttr] ~MyClass() { }
public void F1([param: MyAttr] int i) { }
[method: MyAttr][return: MyAttr] public void F2() { }
[property: MyAttr] public int Prop
{
[method: MyAttr][return: MyAttr] get { return Y; } [return: MyAttr][param: MyAttr] set { Y = value; }
403

}
[event: MyAttr][field: MyAttr] public event Del2 Event1; [event: MyAttr] public event Del2 Event2
{
[return: MyAttr][param: MyAttr] add { } [return: MyAttr][method: MyAttr] remove { }
}
}
При объявлении собственного класса атрибута рекомендуется в его имени использовать окончание Attribute. При использовании атрибута это окончание можно опускать, оно добавляется компилятором автоматически. Если возникает неоднозначность, тип атрибута предваряется символом «@». В этом случае окончание добавляться не будет:
class X : Attribute { }
class XAttribute : Attribute { } class Y : Attribute { }
class ZAttribute : Attribute { }
[X]class MyClass1 {} // Ошибка [@X] class MyClass2 {} // ОК
[XAttribute] class MyClass3 { } // ОК [@XAttribute] class MyClass4 { } // ОК
[Y]class MyClass5 { } // ОК
[Z]class MyClass6 { } // ОК [ZAttribute] class MyClass7 { } // ОК
Пример: Samples\5.4\5_4_1_attr.
5.4.2. Определение и запрос атрибутов
5.4.2.1. Параметры атрибута
Итак, рассмотрим, чем отличаются позиционные и именованные аргументы атрибутов. Позиционные аргументы – это фактические параметры конструктора класса атрибута. Именованные аргументы – это записи вида
<идентификатор> = <выражение>
где идентификатор – это имя нестатического поля или свойства класса атрибута, доступного как для чтения, так и для записи.
Позиционные аргументы должны стоять в начале списка аргументов, и соответствовать порядку формальных аргументов конструктора. После них могут располагаться в произвольном порядке именованные аргументы.
Типы позиционных и именованных аргументов атрибута ограничены следующим набором:
404

•Все типы по значению, кроме decimal, структур и обнуляемых типов. Перечисления допускаются при условии, что они и все типы, по отношению
ккоторым они являются вложенными, открытые (public);
•System.Type;
•object;
•одномерный массив значений любого из вышеуказанных типов. Пример:
public class MyAttr : Attribute
{
public MyAttr(int i) { }
public MyAttr(int i, int j) { } public const int X = 1;
public readonly int Y = 2; public int Z = 3;
public decimal U = 4;
public int A { get { return 0; } } public int B { set { } }
public int C { get; set; } public double D { get; set; } public string Str { get; set; }
}
[MyAttr] public enum E1 { } // Ошибка [MyAttr("x", B = 7)] public enum E2 { } // Ошибка
[MyAttr("x", X = 5, Y = 5)] public enum E3 { } // Ошибка [MyAttr(5, 7, A = 1)] public enum E4 { } // Ошибка
[MyAttr("x", C = 5, D = 0.5, Str = "ABC")] public enum E5 { } // ОК [MyAttr(5, 7, Str = "Привет!", Z = 7)] public enum E6 { } // ОК
[MyAttr("x", U = 1, Str = "Привет!", Z = 7)] public enum E7 { } //
Ошибка
В данном примере класс атрибута имеет два конструктора. Поэтому при описании атрибута должны быть обязательно указаны позиционные аргументы – один параметр типа string или два параметра типа int. В качестве именованных аргументов можно в произвольном порядке указывать Z, C, D, Str. Они имеют допустимый тип и доступ для чтения и записи.
Пример: Samples\5.4\5_4_2_params.
Давайте рассмотрим более конкретный вариант определения атрибутов. Предположим, мы хотим для каждого поля некоторого класса определить его формат и цвет текста и фона при выводе на консоль. Опишем следующие классы атрибутов:
class FormattableAttribute : Attribute
{
}
class FormatAttribute : Attribute
{
405

private string faFormat;
private ConsoleColor faColor = ConsoleColor.Gray; private ConsoleColor faBackground = ConsoleColor.Black;
public FormatAttribute(string format)
{
faFormat = format;
}
public string Format
{
get { return faFormat; }
}
public ConsoleColor Color
{
get { return faColor; } set { faColor = value; }
}
public ConsoleColor Background
{
get { return faBackground; } set { faBackground = value; }
}
}
Первый класс атрибута не содержит параметров, и будет указываться лишь для того, чтобы показать, что тип имеет атрибуты форматирования полей. Второй класс будет использоваться для указания формата вывода полей типа. Здесь требуется обязательное указание формата, а указание цвета символов и фона опционально (по умолчанию – светло-серый и черный соответственно):
[Formattable] class MyClass
{
[Format("{0:P2}", Color = ConsoleColor.Blue)] public double Per = 0.7581;
[Format("{0}", Background = ConsoleColor.Red)] public string Str = "Hello!";
[Format("0x{0,4:X4}", Color = ConsoleColor.Yellow, Background = ConsoleColor.Blue)]
public int Val = 123;
}
5.4.2.2. Запрос атрибутов
Для запроса типа или члена о прикрепленных к ним атрибутах применяется отражение. Способ получения атрибута зависит от типа члена, к которому производится запрос:
1. Метод GetCustomAttributes класса Type возвращает атрибуты типа (все или только атрибуты указанного класса атрибутов).
406

2.Метод GetCustomAttributes класса MemberInfo возвращает атрибуты члена типа. Этот метод перегружен для различных видов членов типа в клас- сах-потомках MemberInfo (см. п. 5.3.1).
3.Методы GetCustomAttribute и GetCustomAttributes класса Attribute
возвращают атрибуты сборки, модуля, типа или члена типа. У первых двух методов две реализации:
object[] GetCustomAttributes(bool inherit);
object[] GetCustomAttributes(Type attributeType, bool inherit);
Первая реализация возвращает массив всех атрибутов типа или члена, вторая – только атрибутов указанного класса (или производных от него классов атрибутов). Логический параметр определяет, следует ли дополнительно искать атрибуты у базовых типов (для метода Type.GetCustomAttributes) или у членов базовых типов, которые переопределяются данным членом (для ме-
тода MemberInfo.GetCustomAttributes). У методов класса Attribute появляется дополнительные параметры – ссылки на метаклассы сборки, модуля, типа или члена типа.
Пример:
static void GetAttribute(object x)
{
FormattableAttribute fattr = (FormattableAttribute)Attribute.GetCustomAttribute(x.GetType(), typeof(FormattableAttribute));
if (fattr != null)
{
FieldInfo[] fields = x.GetType().GetFields();
foreach (FieldInfo field in fields)
{
FormatAttribute attr = (FormatAttribute)Attribute.GetCustomAttribute(field, typeof(FormatAttribute));
if (attr != null)
{
Console.WriteLine("Формат: {0}", attr.Format); Console.WriteLine("Цвет: {0}", attr.Color); Console.WriteLine("Фон: {0}", attr.Background);
Console.BackgroundColor = attr.Background; Console.ForegroundColor = attr.Color; Console.WriteLine(String.Format(attr.Format,
field.GetValue(x)));
Console.ResetColor();
}
}
}
}
407

В данном примере сначала мы проверяем, что тип класса «x» описан с атрибутом FormattableAttribute. Если это так, то, используя отражение, перебираем все поля этого класса и ищем среди них те, что описаны с атрибутом FormatAttribute. После этого используем параметры атрибута для вывода на консоль значения поля. Так, при следующем вызове данного метода
static int Main()
{
MyClass cls = new MyClass();
GetAttribute(cls); return 0;
}
вывод на консоль будет следующим:
Формат: {0:P2} Цвет: Blue
Фон: Black
75,81% // синий шрифт Формат: {0}
Цвет: Gray Фон: Red
Hello! // красный фон, серый шрифт Формат: 0x{0,4:X4}
Цвет: Yellow Фон: Blue
0x007B // синий фон, желтый шрифт
Пример: Samples\5.4\5_4_2_request.
5.4.3. Атрибут AttributeUsage
C помощью атрибута AttributeUsage, если его использовать при описании класса атрибута, можно определить допустимые способы его применения. Синтаксис:
"["AttributeUsage(<значение> [, AllowMultiple = <значение>] [, Inherited
= <значение>])"]"
Пример: Samples\5.4\5_4_3_usage.
5.4.3.1. Указание целевых объектов атрибута
Первый параметр является позиционным. Он позволяет задавать целевые объекты, к которым может быть прикреплен атрибут. Тип этого параметра – перечисление AttributeTargets (табл. 5.4).
Табл. 5.4 – Константы перечисления AttributeTargets
408

Константа |
Значение |
Описание |
|
|
|
Assembly |
0x0001 |
Атрибут применим к сборке |
|
|
|
Module |
0x0002 |
Атрибут применим к модулю |
|
|
|
Class |
0x0004 |
Атрибут применим к классу |
|
|
|
Struct |
0x0008 |
Атрибут применим к структуре |
|
|
|
Enum |
0x0010 |
Атрибут применим к перечислению |
|
|
|
Constructor |
0x0020 |
Атрибут применим к конструктору |
|
|
|
Method |
0x0040 |
Атрибут применим к методу |
|
|
|
Property |
0x0080 |
Атрибут применим к свойству или индекса- |
|
|
тору |
|
|
|
Field |
0x0100 |
Атрибут применим к полю |
|
|
|
Event |
0x0200 |
Атрибут применим к событию |
|
|
|
Interface |
0x0400 |
Атрибут применим к интерфейсу |
|
|
|
Parameter |
0x0800 |
Атрибут применим к параметру метода |
|
|
|
Delegate |
0x1000 |
Атрибут применим к делегату |
|
|
|
ReturnValue |
0x2000 |
Атрибут применим к возвращаемому значе- |
|
|
нию |
|
|
|
GenericParameter |
0x4000 |
Атрибут применим к универсальному пара- |
|
|
метру |
|
|
|
All |
0x7fff |
Атрибут применим к любому элементу про- |
|
|
граммы |
|
|
|
Несколько значений можно объединять операцией дизъюнкции. Укажем для классов атрибутов, описанных нами выше, что атрибут FormattableAttribute может применяться только к классам, а атрибут
FormatAttribute – к полям класса:
[AttributeUsage(AttributeTargets.Class)] class FormattableAttribute : Attribute // ...
[AttributeUsage(AttributeTargets.Field)] class FormatAttribute : Attribute
// ...
Теперь попытка использовать атрибут не по назначению будет приводить к ошибке компиляции:
[Format("G")] class MyClass2 { } // Ошибка
409

5.4.3.2. Атрибуты однократного и многократного использования
Второй параметр разрешает (если его значение равно true) или запрещает (false, по умолчанию) многократное применение атрибута. Например, разрешим задание для поля сразу нескольких атрибутов FormatAttribute:
[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] class FormatAttribute : Attribute
// ...
Теперь его можно применять несколько раз к одному и тому же полю, а атрибут FormattableAttribute может быть только единожды применен к одному классу:
[Formattable] class MyClass
{
[Format("{0:P2}", Color = ConsoleColor.Blue)] [Format("{0:E}", Color = ConsoleColor.Green)] public double Per = 0.7581;
// ...
}
[Formattable, Formattable] class MyClass3 { } // Ошибка
Теперь для запроса атрибутов нельзя применять использованный нами ранее метод Attribute.GetCustomAttribute – он возвращает ссылку на атрибут только в том случае, если он один. Иначе возникает исключение
Необработанное исключение: System.Reflection.AmbiguousMatchException:
Обнаружено несколько пользовательских атрибутов одного типа.
Поэтому используем метод MemberInfo.GetCustomAttributes, который возвращает массив атрибутов:
FormatAttribute[] attrs = (FormatAttribute[])field.GetCustomAttributes(typeof(FormatAttribute), false);
foreach(var attr in attrs)
{
Console.WriteLine("Формат: {0}", attr.Format); Console.WriteLine("Цвет: {0}", attr.Color); Console.WriteLine("Фон: {0}", attr.Background);
Console.BackgroundColor = attr.Background; Console.ForegroundColor = attr.Color; Console.WriteLine(String.Format(attr.Format, field.GetValue(x))); Console.ResetColor();
}
Результат работы программы:
Формат: {0:E}
Цвет: Green Фон: Black
7,581000E-001 // ярко-зеленый шрифт
410