Атрибуты
ГЛАВА 8
Атрибуты
Большинство языков программирования разрабатываются с учетом набора необходимых возможностей. Так, в начале создания компилятора вы думаете, какова будет структура приложений на новом языке, как один фрагмент кода будет вызывать другой, как распределить функциональность и о многих других проблемах, решение которых сделает язык продуктивным средством создания ПО. Обычно разработчикам компиляторов приходится иметь дело со статическими сущностями. Например, вы определяете класс на С#, помещая перед его именем ключевое слово class. После этого вы определяете производный класс, вставляя двоеточие после его имени, за которым следует имя базового класса. Это может служить примером решения, которое принимается разработчиком языка однажды и после этого не может быть изменено.
Сейчас те, кто пишет компиляторы, семь раз отмерят, прежде чем отрежут. Но даже они не могут предвидеть все будущие усовершенствования в нашей области и то, как они повлияют на способы выражения программистами типов на данном языке. Скажем, как создать связь между классом на C++ и URL документации для данного класса? Или как вы будете ассоциировать члены классов C++ с полями XML в новом решении вашей компании в области электронной коммерции? Поскольку C++ разрабатывался задолго до прихода в нашу жизнь Интернета и протоколов, таких как XML, обе эти задачи выполнить довольно трудно.
До сих пор решения подобных проблем предполагают хранение дополнительной информации в отдельном файле (DEF, IDL и т. д.), которая затем связывается с тем или иным типом или членом. Так как компилятор не обладает сведениями о каком-то файле или связи между вашим классом и этим файлом, такой подход обычно называется "разрывным решением" (disconnected solution). Главная проблема в том, что класс больше не является "самоописывающимся", т. е. теперь пользователь не может сказать о классе все, лишь взглянув на определение класса. Одно из преимуществ самоописывающегося компонента — гарантия соблюдения при компиляции и в период выполнения правил, ассоциированных с компонентом. Кроме того, сопровождение самоописывающегося компонента проще, поскольку разработчик может найти всю связанную с ним информацию в одном месте.
Так все и было многие десятилетия. Разработчики языка пытаются определить, что вы хотите от языка, и создают компилятор с этими возможностями, и, к счастью или к сожалению, вы остаетесь с этими возможностями до прихода следующего компилятора. Так это и есть вплоть до сегодняшнего дня. С# предлагает иную парадигму, берущую начало от атрибутов (attributes).
Атрибуты предоставляют универсальные средства связи данных (в виде аннотаций) с типами, определенными на С#. Вы можете применять их для определения информации периода разработки (например, документации), периода выполнения (например, имя столбца БД) или даже характеристик поведения периода выполнения (например, может ли данный член участвовать в транзакции). Возможности атрибутов бесконечны. Поскольку вы можете создавать атрибуты на основе любой информации, существует стандартный механизм определения самих атрибутов и запроса членов или типов в период выполнения как связанных с ними атрибутов.
Лучше объяснить использование атрибутов на примере. Допустим, у вас есть приложение, хранящее некоторые данные в реестре. Одна из проблем разработки связана с выбором места хранения информации о разделе реестра. В большинстве сред разработки она, как правило, хранится в файле ресурсов, в константах или даже жестко запрограммирована в вызовах API реестра. Однако мы снова имеем ситуацию, когда неотъемлемая часть класса хранится отдельно от определения остальной части класса. Атрибуты позволяют "прикреплять" эту информацию к членам класса, получая полностью самоописывающийся компонент. Вот пример, иллюстрирующий, как это может выглядеть, если предположить, что атрибут RegistryKey уже определен:
class MyClass {
[RegistryKey(HKEY_CURRENT_USER, "foo")] public int Foo; }
Чтобы прикрепить определенный атрибут к типу или члену С#, нужно просто задать данные атрибута в скобках перед целевым типом или членом. В нашем примере мы прикрепили атрибут RegistryKey к полю MyClass.Foo. Как вы вскоре увидите, все, что нам надо сделать в период выполнения, — это запросить значение поля, связанное с разделом реестра и использовать его, чтобы сохранить дату в реестре.
В предыдущем примере синтаксис прикрепления атрибута к типу или члену похож на тот, что применяется при создании экземпляра класса. Дело в том, что атрибут на самом деле является классом, производным от базового класса System.Attribute.
А теперь немного расширим атрибут RegistryKey:
public enun RegistryHives {
HKEY_CLASSES_ROOT,
HKEY_CURRENT_USER.
HKEY_LOCAL_MACHINE,
HKEY.USERS,
HKEY_CURRENT_CONFIG }
public class RegistryKeyAttribute : Attribute
{
public RegistryKeyAttribute(RegistryHives Hive, String ValueName)
{
this.Hive = Hive; this.ValueName = ValueNane; >
protected RegistryHives hive; public RegistryHives Hive {
get { return hive; }
set { hive = value; } >
protected String valueNane; public String ValueName
<
get { return valueName; >
set { valueName = value; } } >
В этом примере я добавил епит для различных типов разделов реестра, конструктор для класса-атрибута (который принимает тип и имя раздела реестра) и два свойства для имени улья реестра и имени значения. Теперь мы видим более широкие возможности атрибутов, так что посмотрим, как запросить атрибут в период выполнения. Мы будем работать с полностью функциональным примером. Перейдем к рассмотрению некоторых более сложных вопросов, связанных с определением и прикреплением атрибутов.
ПРИМЕЧАНИЕ
В приведенных примерах к именам классов атрибутов добавлено слово Attribute. Однако, прикрепляя атрибут к типу или члену, я отбросил этот суффикс. Это одна из возможностей сокращения, встроенная разработчиками С#, и она дается нам даром. Обнаружив атрибут, прикрепленный к типу или члену, компилятор будет искать класс с именем заданного атрибута, производный от System.Att-ribute. Если класс найти не удастся, компилятор добавит к имени заданного атрибута слово Attribute и будет продолжать поиск получившегося имени. Поэтому в повседневной практике имена классов атрибутов при определении оканчиваются словом Attribute. Впоследствии эта часть имени опускается.
Мы знаем, как определять атрибуты В виде производных от System.Attri-bute и как прикреплять их к типу или члену. А что теперь? Как использовать атрибуты при программировании? Короче, как производится запрос типа или члена как прикрепленных к ним атрибутов (и их параметров)?
Чтобы запросить тип или член о прикрепленных к ним атрибутах, нужно применить отражение (reflection). Об отражении речь пойдет в главе 16, поэтому здесь я коснусь его лишь в той мере, в какой это необходимо, чтобы проиллюстрировать получение информации об атрибутах в период выполнения.
Отражение — это функция, позволяющая в период выполнения динамически определять характеристики типов в приложении. Например, с помощью Reflection API из состава .NET Framework можно циклически запрашивать метаданные всего приложения и создавать списки определенных для него классов, типов и методов. Рассмотрим несколько примеров атрибутов и способов их запроса с помощью отражения.
Способ получения атрибута зависит от типа члена, к которому производится запрос. Допустим, вам нужно узнать атрибут, определяющий удаленный сервер, на котором должен быть создан объект. Без атрибутов вам бы пришлось сохранять эту информацию в константе или файле ресурсов приложения. А используя их, можно просто создать аннотацию для класса с именем удаленного сервера, например, так:
using System;
public enum RemoteServers
{
JEANVALJEAN,
JAVERT,
COSETTE }
public class RemoteObjectAttribute : Attribute
{
public RemoteObjectAttribute(RemoteServers Server)
{
this, server = Server;
}
protected RemoteServers server;
public string Server
<
get
{
return RemoteServers.GetName(typeof(RemoteServers),
this, server);
} } }
t RemoteObj ect(RemoteSe rve rs.COSETTE)]
class MyRemotableClass
{
}
Сервер, на котором нужно создавать объект, можно определить так:
class ClassAttrApp {
public static void Main() {
Type type = typeof(MyReraotableClass);
foreach (Attribute attr in type.GetCustomAttributesQ)
{
RemoteObjectAttribute remoteAttr = attr as RemoteObjectAttribute;
if (null != remoteAttr) {
Console.WriteLine("Co3flai*Te этот объект на {0}.",
remoteAttr. Server); } } } }
Как можно ожидать, приложение выдаст следующее: Создайте этот объект на COSETTE.
Поскольку все вариации этого примера будут использовать общий код, рассмотрим, что здесь происходит с точки зрения отражения и как оно возвращает значение атрибута в период выполнения.
Первая строка в методе Main использует оператор typeof:
Type type = typeof(MyRemotableClass);
Этот оператор возвращает ассоциированный с типом объект System. Туре, который передается как его единственный аргумент. Как только этот объект оказался в вашем распоряжении, к нему можно выполнить запрос.
Относительно следующей строки в пояснении нуждаются два момента:
foreach (Attribute attr in type.GetCustomAttributes(true))
Первый — вызов метода Ту ре.GetCustomAttributes. Этот метод возвращает массив значений типа Attribute, который в этом случае будет содержать все атрибуты, прикрепленные к классу MyRemotableClass. Второй — оператор foreach, циклически обрабатывающий возвращенный массив, помещая каждое последовательное значение в переменную attr типа Attribute.
Следующее выражение использует оператор as, чтобы попытаться преобразовать переменную attr в тип RemoteObjectAttribte:
RemoteObjectAttribute remoteAttr = attr as RemoteObjectAttribute;
Далее выполняется проверка на пустое значение, которое указывает на наличие сбоя при использовании оператора as. Если значение не пустое, значит, переменная remoteAttr содержит верный атрибут, прикрепленный к типу MyRemotableClass — мы вызываем одно из свойств RemoteObjectAttribute, чтобы вывести имя удаленного сервера:
if (null != remoteAttr) <
Console.WriteLine("Создайте этот объект на {0}",
remoteAttr. Server); }
Код, применяющий отражение для запроса атрибута метода, отличается от кода для запроса атрибута класса. В этом примере мы зададим атрибут, который будет использован для определения метода, способного принимать участие в транзакциях:
using System;
using System.Reflection;
public class TransactionableAttribute : Attribute {
public TransactionableAttributeO
{
> }
class TestClass {
[Transactionable]
public void Foo()
{>
public void Bar() {}
[Transactionable] public void Baz() {} >
class MethodAttrApp
{
public static void Main() {
Type type = Type.GetType("TestClass");
foreach(MethodInfo method in type.GetMethodsQ) {
foreach (Attribute attr in
method.GetCustomAtt ributes()) {
if (attr is TransactionableAttribute) {
Console.WriteLine("{0} может участвовать " +
"в транзакции.", method.Name); } } } > }
Этот код выводит следующую информацию:
Foo может участвовать в транзакции.
Baz может участвовать в транзакции.
В данном примере достаточно простого присутствия TransactionableAttribute, чтобы сообщить коду о том, что метод, для которого указан этот атрибут, может принимать участие в транзакции. Поэтому не определено ни одного его члена, кроме "голого" конструктора без параметров.
TestClass определен с тремя методами (Foo, Bar и Baz), два из которых определены как способные принимать участие в транзакциях. Заметьте: для прикрепления атрибута с конструктором, не принимающим параметров, открывающую и закрывающую скобки вводить не требуется.
Дальше еще интересней. Давайте приглядимся к способу запроса методов класса как их атрибутов. Начнем мы с использования GetType, статического метода Туре, для получения объекта System. Type класса TestClass:
Type type = Type.GetType("TestClass");
Затем мы вызовем метод Type.GetMethods, чтоб получить массив объектов Methodlnfo. Каждый из этих объектов содержит данные о методе класса TestClass. Мы обработаем каждый метод оператором foreach:
foreach(Method!nfo method in type.GetMethodsO)
Теперь, имея объект Methodlnfo, мы можем, вызвав метод Methodlnfo.-GetCustomAttributes, получить все атрибуты метода, созданные пользователем. Мы снова произведем циклическую обработку возвращенного массива объектов оператором foreach:
foreach (Attribute attr in method.GetCustomAttributes(true))
К данному моменту выполнения программы у нас уже есть атрибут метода. Теперь с помощью оператора is мы сделаем к нему запрос, является ли он атрибутом TransactionableAttribute. Если это так, мы выводим имя метода:
if (attr is TransactionableAttribute) {
Console.Writel_ine("{0} может участвовать в транзакции.",
method.Name); }
В качестве последнего примера запроса членов как прикрепленных к ним атрибутов мы рассмотрим способ запроса полей класса. Допустим, наш класс содержит поля, значения которых нужно сохранить в реестре. Для этого можно определить атрибут с конструктором, принимающим как параметр епит с ульями реестра, и строку, представляющую имя параметра реестра. Затем вы можете выполнить запрос к полю, как к разделу реестра:
using System;
using System.Reflection;
public enum RegistryHives {
HKEY_CLASSES_ROOT,
HKEY_CURRENT_USER,
HKEY_LOCAL_MACHINE,
HKEYJJSERS,
HKEY_CURRENT_CONFIG >
public class RegistryKeyAttribute : Attribute <
public RegistryKeyAttribute(RegistryHives Hive, String ValueName)
{
this.Hive = Hive; this.ValueName = ValueName; }
protected RegistryHives hive; public RegistryHives Hive {
get { return hive; }
set { hive = value; } >
protected String valueName; public String ValueName {
get { return valueName; }
set { valueName = value; } } }
class TestClass <
[RegistryKey(RegistryHives.HKEY_CURRENT_USER, "Foo")]
public int Foo;
public int Bar; }
class FieldAttrApp {
public static void Main() {
Type type = Type.GetTypeC'TestClass");
foreach(Field!nfo field in type.GetFieldsQ) {
foreach (Attribute attr in field.GetCustomAttributesO) {
F.agistryKeyAttribute registryKeyAttr =
attr as RegistryKeyAttribute; if (null != registryKeyAttr) {
Console.WriteLine
("{0} будет сохранен в ... {2}", field.Name,
registryKeyAttr.Hive, registryKeyAttr.ValueName); } } } } }
Я не буду описывать выполнение каждого этапа этого кода, так как он в чем-то дублирует предыдущий пример. Однако пара деталей все же важна для нас. Во-первых, как и объект Methodlnfo, определенный для получения информации о методе из объекта типа, объект Fieldlnfo предоставляет аналогичную функциональность для получения из объекта сведений о поле. Как и в предыдущем примере, мы начнем с получения объекта типа, ассоциированного с нашим тестовым классом. Затем мы циклически обработаем массив Fieldlnfo, а также все атрибуты каждого объекта Fieldlnfo, пока не найдем нужный — RegistryKeyAttribute. Если мы его обнаружим, то выведем имя поля и значения полей атрибута Hive и ValueName.
В вышеприведенном примере я рассказал об использовании прикрепленных атрибутов с помощью их конструкторов. А теперь рассмотрим некоторые вопросы, связанные с конструкторами атрибутов, которых я не касался раньше.
В примере FieldAttrApp (см. выше) вы могли видеть атрибут с именем RegistryKeyAttribute. Его конструктор имел такой вид:
public RegistryKeyAttribute(RegistryHives Hive, String ValueName)
Далее к полю был прикреплен атрибут на основе сигнатуры этого конструктора:
[RegistryKey(RegistryHives.HKEY_CURRENT_USER, "Foo")] public int Foo;
Пока все просто. У конструктора есть два параметра, и два параметра использовались, чтобы прикрепить этот атрибут к полю. Однако мы можем упростить код. Если параметр большую часть времени останется неизменным, зачем каждый раз заставлять пользователя класса типизировать его? Мы можем установить значения по умолчанию, применив позиционные и именованные параметры.
Позиционными называются параметры конструктора атрибута. Они обязательны и должны задаваться каждый раз при использовании атрибута. В нашем примере Registry Key Attribute позиционными являются оба параметра, Hive и ValueName. Именованные параметры на самом деле не определяются в конструкторе атрибута. Они скорее представляют собой нестатические поля и свойства. Поэтому именованные параметры позволяют клиенту устанавливать значения полей и свойств атрибута при создании его экземпляра, не требуя от вас создания конструктора для каждой возможной комбинации полей и свойств, значения которых может понадобиться установить клиенту.
Каждый открытый конструктор может определять последовательность позиционных параметров. Это верно и в отношении любого типа класса. Но в случае атрибутов после указания позиционных параметров пользователь может ссылаться на некоторые поля или свойства, применяя синтаксис Имя_поля_или_свойства=3начение. Чтобы проиллюстрировать это, изменим атрибут Registry Key Attribute. Мы создадим лишь один позиционный параметр RegistryKeyAttribute. ValueName, а необязательным именованным параметром будет Registry KeyAttribute. Hive. Итак, возникает вопрос: "Как определить что-либо как именованный параметр?" Поскольку в определение конструктора включены только позиционные — и поэтому необходимые — параметры, просто удалите параметр из определения конструктора. Впоследствии пользователь может указывать как именованный параметр любое поле, не являющееся readonly, static или const, или любое поле, у которого есть метод-аксессор для установки его значения, или установщик, который не является статическим. Поэтому чтобы сделать Registry Key Attribute. Hive именованным параметром, мы уберем его из определения конструктора, так как он уже существует в виде открытого свойства, доступного для чтения и записи:
public RegistryKeyAttribute(String ValueName)
Теперь пользователь может прикрепить атрибут любым из следующих способов:
[RegistryKeyC'Foo")]
[RegistryKeyCToo", Hive = RegistryHives.HKEY_LOCAL_MACHINE)]
Это дает вам гибкость, обеспечиваемую наличием у поля значения по умолчанию, в то же время предоставляя пользователю возможность изменять это значение при необходимости. Секунду! Если пользователь не устанавливает значения поля Registry Key Attribute. Hive, как мы установим для него значение по умолчанию? Вы можете подумать: "Хорошо, посмотрим, не установлено ли оно в конструкторе". Однако проблема в том, что Registry KeyAttribute. Hive — это епит, в основе которого лежит int — размерный тип. Это значит, что по умолчанию компилятор инициализирует его значением 0! Если мы изучим значение RegistryKey-Attribute.Hive в конструкторе и найдем его равным 0, мы не сможем узнать, установлено ли оно вызывающим кодом через именованный параметр или инициализировано компилятором как размерный тип. К сожалению, единственный известный мне пока способ решения этой проблемы — это изменить код так, чтобы значение, равное 0, стало неверным. Это можно сделать, изменив RegistryHives епит:
public enum RegistryHives {
HKEY_CLASSES_ROOT = 1,
HKEY_CURRENT_USER,
HKEY_LOCAL_MACHINE,
HKEY_USERS,
HKEY_CURRENT_CONFIG }
Теперь мы знаем, что единственный способ, позволяющий Registry-Key Attribute. Hive быть равным 0, — инициализация его компилятором этим значением, если после этого пользователь не изменил его значение через именованный параметр. Сейчас мы можем написать для инициализации код вроде этого:
public RegistryKeyAttribute(String ValueName) <
if (this.Hive == 0)
this.Hive = RegistryHives.HKEY_CURRENT_USER;
this.ValueName = ValueName; }
Используя именованные параметры, вы должны указать сначала позиционные параметры. После этого именованные параметры можно указать в любом порядке, так как перед ними идет имя поля или свойства. При компиляции приведенного ниже примера возникнет ошибка компилятора:
// Это ошибочный код, поскольку позиционные параметры не могут // стоять после именованных параметров. [RegistryKey(Hive=RegistryHives.HKEY_LOCAL_MACHINE, "Foo")]
Кроме того, вы не можете именовать позиционные параметры. При компиляции атрибутов компилятор сначала попытается разрешить именованные параметры, затем разрешить оставшиеся — именованные — параметры с помощью сигнатуры метода. Хотя компилятор сможет разрешить каждый именованный параметр, следующий код не будет компилироваться, так как по завершении разрешения именованных параметров компилятор не найдет ни одного позиционного параметра и выдаст сообщение "No overload for method 'Registry Key Attribute' takes '0" arguments"'.
[RegistryKey(ValueName="Foo", Hive=RegistryHives.HKEY_LOCAL_MACHINE)]
Наконец, именованными параметрами могут быть любые открытые поля или свойства, включая метод-установщик, не определенные как static или const.
Типы позиционных и именованных параметров класса атрибута ограничены следующим набором:
- bool, byte, char, double, float, int, long, short, string;
- System.Types;
- object;
- enum — при условии, что он и все типы, по отношению к которым он является вложенным, открытые (как в примере, где используется перечисление ульев реестра);
- одномерный массив значений любого из вышеуказанных типов.
Поскольку набор типов параметров ограничен приведенным выше списком, вы не можете передавать конструктору структуры данных наподобие класса. Это ограничение имеет смысл, так как атрибуты прикрепляются в период разработки и в это время у вас не будет созданного экземпляра класса (объекта). Допускается применение вышеуказанных типов, так как они позволяют жестко запрограммировать их значения во время разработки.
Кроме пользовательских параметров, которые вы задаете для аннотации обычных типов С#, с помощью атрибута AttributeUsage можно определить способ применения этих атрибутов. Согласно документации правила вызова атрибута. AttributeUsage таковы:
[AttributeUsage(
validon,
AllowMultiple = allomultlple,
Inherited = inherited
);
Как видите, позиционные параметры легко отличить от именованных. Я настоятельно рекомендую так документировать ваши атрибуты, чтобы у их пользователя не возникало необходимости обращаться к исходному коду класса атрибута для поиска открытых полей, доступных для чтения и записи, которые могут применяться как именованные атрибуты.
А сейчас снова взгляните на атрибут AttributeUsage из предыдущего раздела. Заметьте: параметр validon является позиционным и, естественно, обязательным. Он позволяет задавать типы, к которым может быть прикреплен атрибут. На самом деле тип параметра validon атрибута Attribute-Usage — AttributeTargets, представляющий собой перечисление, определяемое так:
public enum AttributeTargets <
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,
All = Assembly | Module | Class | Struct | Enum |
Constructor | Method | Property | Field | Event |
Interface | Parameter | Delegate, ClassMembers = Class |
Struct | Enum | Constructor | Method | Property | Field |
Event | Delegate | Interface, }
При использовании атрибута Attribute Usage можно задать Attribute-Targets.All. Это позволяет прикрепить атрибут к любому из типов в списке перечисления AttributeTargets. Если вы вообще не определили Attribute-Usage, можете прикрепить атрибут к любому типу — это значение по умолчанию. И тут вы можете спросить: "А зачем вообще значение vali-don?" А затем, что с вашим атрибутом могут применяться именованные параметры, которые вы, может быть, захотите изменить. Помните: используя именованный параметр, вы должны поставить все позиционные параметры перед ним. Это позволяет легко задавать применение атрибутов по умолчанию, определяемое AttriubuteTargets.All, и при этом устанавливать именованные параметры.
Итак, когда и для чего задавать параметр validon (AttributeTargets)! Когда вам нужно точно контролировать способы применения атрибута. В наших примерах мы создали атрибут RemoteObjeci'Attribute, применимый только к классам, атрибут TransactionableAttribute, применимый только к методам, и атрибут Registry Key Attribute, который имеет смысл лишь по отношению к полям. Если мы хотим убедиться, что они были использованы для аннотации только тех типов, для которых они разработаны, мы можем определить их так (для ясности тела атрибутов не приводятся):
[Att ributeUsage(Att ributeTa rgets.Class)] public class RemoteObjectAttribute : Attribute] {
}
[Att rlbuteUsage(AttributeTargets.Method)]
public class TransactionableAttribute : Attribute
{
}
[Att ributeUsage(Att ributeTargets.Field)]
public class RegistryKeyAttribute : Attribute
<
}
И последний момент относительно перечисления AttributeTargets: вы можете комбинировать члены с помощью оператора |. Если у вас есть атрибут, применимый и к полям, и к свойствам, Attribute Usage можно прикрепить так:
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
AttributeUsage позволяет задать атрибуты для одно- и многократного применения. Это определит, как много раз один атрибут может быть задействован с одним полем. По умолчанию все атрибуты используются однократно. Это означает, что при компиляции следующего кода возникнет ошибка компилятора:
using System;
using System.Reflection;
public class SingleUseAttribute : Attribute {
public SingleUseAttribute(String str)
{
> }
// ОШИБКА: возникает ошибка компилятора "duplicate attribute".
[SingleUse("abc")]
[SingleUse("def")]
class MyClass
{
}
class SinglellseApp {
public static void Main()
{
} }
Чтобы исправить эту ошибку, укажите в строке Attribute Usage, что вы хотите разрешить многократное использование атрибута с данным типом. Этот код будет работать:
using System;
using System.Reflection;
[AttributeUsage(AttributeTargets.All, AllowMultiple=true)]
public class SingleUseAttribute : Attribute
{
public SingleUseAttribute(String str)
{
}
>
[SingleUse("abc")]
[SingleUse("def")]
class MyClass
{
>
class MultiUseApp {
public static void Main()
{ } }
Практическим примером использования этого подхода служит атрибут RegistryKeyAttribute (см. раздел "Определение атрибутов"). Поскольку вполне реально, что поле может быть сохранено в нескольких местах реестра, можете прикрепить атрибут Attribute Usage с именованным параметром AllowMultiple, как показано в примере выше.
Последний параметр атрибута Attribute Usage — флаг inherited — определяет, может ли атрибут наследоваться. Его значение по умолчанию равно false. Если же установить флаг inherited в true, его значение зависит от значения флага AllowMultiple. Если inherited установлен в true, a Allow-Multiple — в false, установленный атрибут заменит унаследованный. Однако если и inherited, и AllowMultiple установлен в true, атрибуты члена будут аккумулироваться.
Взгляните на следующий код и попробуйте определить, что аннотирует атрибут — возвращаемое значение или метод:
class MyClass {
[HRESULT]
public long Foo(); }
Если у вас есть опыт работы с СОМ, вы должны знать, что HRESULT — это стандартный возвращаемый тип для всех методов, кроме AddRef или Release. Однако нетрудно заметить, что если имя атрибута применимо и к возвращаемому значению, и к имени метода, то компилятору будут непонятны ваши намерения. Вот несколько сценариев, -в которых компилятор не поймет ваших намерений из контекста:
- метод или возвращаемый тип;
- событие, поле или свойство;
- делегат или возвращаемый тип;
- свойство, аксессор, возвращаемое значение метода-получателя или параметр значения установщика.
В каждом из этих случаев компилятор производит определение на основе того, что считается "наиболее употребительным". Чтобы обойти такой путь принятия решения, используйте идентификаторы атрибута: assembly, module, type, method, property, event, field, param, return.
Чтобы воспользоваться идентификатором атрибута, поставьте перед именем атрибута идентификатор и двоеточие. В примере MyClass, чтобы быть уверенным в том, что компилятор сможет определить HRESULT как атрибут, аннотирующий возвращаемое значение, а не как метод, вы должны задать его следующим образом:
class MyClass {
[return:HRESULT]
public long Foo(); }
Подведем итоги
Атрибуты С# предоставляют механизм аннотирования типов и членов в период разработки информацией, которая позже может быть получена в период выполнения посредством отражения (reflection). Это позволяет вам создавать истинно автономные, самоописывающиеся компоненты, освобождая от необходимости хранения необходимых битов в файлах ресурсов и константах. Преимуществом при этом является большая мобильность компонента, который легче разрабатывать и сопровождать.