Настраиваемые атрибуты

Настраиваемые атрибуты (custom attributes) - один из новаторских механизмов .NET Framework. Они позволяют снабжать код декларативными аннотациями, наделяя его особыми возможностями. Атрибуты дают возможность задать информацию к любой записи таблицы метаданных, а затем эту информацию можно получить в коде во время выполнения.

Сфера применения настраиваемых атрибутов

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

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

Атрибуты представляют собой лишь средство передачи некоторой дополнительной информации, компилятор помещает эту информацию в метаданные, в остальном - они для него не имеют смысла.

В C# имена настраиваемых атрибутов помещаются в квадратные скобки непосредственно перед именем класса, объекта и т. п.

CLR позволяет применять атрибуты ко всему, что может быть представлено метаданными. Чаще всего они применяются к записям в следующим таблицах: TypeDef (любые создаваемые типы), MethodDef (конструкторы), ParamDef, FieldDef, PropertyDef, EventDef, AssemblyDef. В частности, C# позволяет применять атрибуты к коду, определяющему сборки, модули, типы, поля, методы, параметры методов, возвращаемые значения, свойства, события, параметры обобщённого типа. Можно задать префикс, указывающий явно, к чему будет применён атрибут, однако компилятор способен сделать это и сам. Обязательные префиксами являются assemble, module, return, field и method.

using System;
[assembly: SomeAttr] // Применяется к сборке
[module: SomeAttr] // Применяется к модулю

[type: SomeAttr] // Применяется к типу
internal sealed class SomeType<[typevar: SomeAttr] T> // Применяется к переменной обобщенного типа
{ 
  [field: SomeAttr] // Применяется к полю
  public Int32 SomeField = 0;

  [return: SomeAttr] // Применяется к возвращаемому значению
  [method: SomeAttr] // Применяется к методу
  public Int32 SomeMethod([param: SomeAttr] Int32 SomeParam) // Применяется к параметру
  {
    return SomeParam;
  }

  [property: SomeAttr] // Применяется к свойству
  public String SomeProp
  {
    [method: SomeAttr] // Применяется к механизму доступа get
    get { return null; }
  }

  [event: SomeAttr] // Применяется к событиям
  [field: SomeAttr] // Применяется к полям, созданным компилятором
  [method: SomeAttr] // Применяется к созданным компилятором методам add и remove
  public event EventHandler SomeEvent;
}

Настраиваемый атрибут - всего лишь экземпляр типа. Для соответствия CLS он должен явно или косвенно наследоваться от абстрактного класса System.Attribute. В C# допустимы только CLS-совместимые атрибуты.

При определении атрибута компилятор позволяет опускать суффикс Attribute, что позволяет сделать код более читаемым.

Так как атрибут является экземпляром класса, то класс должен иметь открытый конструктор. По факту, синтаксис применения атрибута аналогичен вызову конструктора. Кроме того, используемый язык может поддерживать специальный синтаксис для определения открытых полей атрибута.

[DllImport("Kernel32", CharSet = CharSet.Auto, SetLastError = true)]

В приведённом выше примере конструктор атрибута принимает один строковый параметр ("Kernel32"), такие параметры называются позиционными (positional parameters) и они являются обязательными. После этого через запятую можно присвоить значения открытым полям, такие параметры называются именованными (named parameters) и являются необязательными.

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

Определение класса атрибутов

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

Если при определении класса атрибутов забыть применить атрибут, указывающий на область действия, то создаваемый атрибут можно будет применить к любым элементам, но только единожды. Кроме того, он будет наследуемым.

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

Конструктор атрибута и типы данных полей и свойств

Определяя конструктор экземпляров класса атрибутов стоит ограничиться примитивными типами, а также можно использовать тип Type. Кроме того, можно передавать SZ-массивы, хотя в таком случае атрибут не будет CLS-совместимым. Применяя атрибут, следует указать постоянное выражение. Пример:

using System;

internal enum Color { Red }

[AttributeUsage(AttributeTargets.All)]
internal sealed class SomeAttribute : Attribute
{
  public SomeAttribute(String name, Object o, Type[] types)
  {
    // 'name' ссылается на String
    // 'o' ссылается на один из легальных типов (упаковка при необходимости)
    // 'types' ссылается на одномерный массив Types с нулевой нижней границей
  }
}

[Some("Jeff", Color.Red, new Type[] { typeof(Math), typeof(Console) })]
internal sealed class SomeType { }

Обнаружив атрибут, компилятор создаёт экземпляр класса, передавая конструктору все указанные параметры. Затем он присваивает значения открытым полям и свойствам. Инициализировав объект, компилятор сериализует его и сохраняет в таблице метаданных.

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

Выявление настраиваемых атрибутов

Само по себе определение атрибута бесполезно, так как это приведёт только к появлению дополнительных метаданных в сборке. Наличие атрибутов должно переопределять некоторое поведение типа. Для этого во время выполнения происходит проверка, которая с помощью отражения (рефлексии, reflection) выясняет наличие определённого атрибута и вызывает соответствующий код

Проверить наличие атрибута в FCL можно различными способами. Для объектов System.Type можно использовать метод IsDefined(). Однако для сборки, модуля или метода это не сработает. Для всех CLS-совместимых атрибутов базовыми являются методы класса System.Reflection.CustomAttributesExtensions. В нём имеется три метода, каждый из которых имеет перегруженные версии.

Если нужно установить только сам факт наличия атрибутам, используется метод IsDefined(), так как он самый быстрый за счёт того, что не десериализует данные.

Для создания объектов атрибутов используются методы GetCustomAttributes() и GetCustomAttribute(). Они отличаются тем, что первый позволяет находит атрибуты, для которых разрешено множественное объявление (то есть для который AllowMultiple равно true). При каждом вызове этих методов создаются экземпляры атрибутов, которые содержат в себе поля и свойства, и возвращаются ссылки на созданные объекты.

Эти методы просматривают данные управляемого модуля и сравнивают строки в поиске указанного класса атрибута. Эти операции требуют времени, так что стоит подумать о кэшировании, если волнует быстродействие.

В пространстве имён System.Reflection находятся классы, позволяющие анализировать содержимое метаданных модуля, а также соответствующие им билдеры. Все эти классы содержат методы IsDefined() и GetCustomAttributes(). Последний метод возвращает массив экземпляров Object, а не Attribute, так как некоторые классы атрибутов могут не соответствовать CLS. Хотя в реальности это может происходить довольно редко.

Есть ещё один аспект, связанный с данными методами. Они ищут класс атрибута и производные от него. Для поиска конкретного класса требуется дополнительная проверка. Или можно сделать класс атрибута запечатанным.

Сравнение экземпляров атрибута

Сравнение атрибутов похоже на сравнение значимых типов. Переопределённый метод Equals() определяет совпадают ли типы и, если типы совпадают, проверяют каждое поле через рефлексию. То есть для повышения производительности стоит также переопределить данный метод в собственных классах настраиваемых атрибутов.

Выявление настраиваемых атрибутов без создания объектов, производных от Attribute

При вызове методов для получения атрибутов из типа Attribute вызывается конструктор класса атрибута и методы, задающие значения свойств. А первое обращение к типу заставляет CLR вызвать статический конструктор (если он определён). Конструктор, методы доступа set и методы статического конструктора могут содержать код, выполняющийся при каждом поиске атрибута. Возможность выполнения в домене неизвестного кода создаёт потенциальную угрозу безопасности.

Для обнаружения атрибутов без выполнения кода класса используется статический метод System.Reflection.CustomAttributeData.GetCustomAttribures(). Отличие этого метода от ранее рассмотренных в том, что он возвращает именно информацию об атрибуте, а не создаёт экземпляр его типа и соответственно, не вызывает никаких методов.

Условные атрибуты

Иногда могут возникать ситуации, когда атрибут нужен не всегда. Для этого к классу создаваемого атрибута можно применить атрибут System.Diagnostics.ConditionalAttribute. В этом случае создаваемый класс называется условным атрибутом (conditional attribute). Для такого атрибута компилятор будет помещать информацию в метаданные, только если при компиляции будет определён соответствующий идентификатор.