воскресенье, 20 января 2013 г.

Атрибуты: откуда их брать

Очередной пост из серии "Что такое хорошо и что такое плохо?". В этот раз о том, как правильно извлекать .NET-атрибуты из метаданных. К сожалению, многие для этой задачи используют интерфейс ICustomAttributeProvider, точнее его реализации в классе Assembly и наследниках класса MemberInfo: Type, PropertyInfo и других.

Для извлечения метаданных в .NET-фреймворке есть несколько различных API, но они не повторяют друг друга (одно не может быть заменено другим) и не подразделяются на "просто более и менее удобные". Каждое из этих API позволяет работать с метаданными на определённом уровне абстракции и служит конкретной своей цели и при выборе API, с которым вы будете работать, нужно исходить именно из этих целей, а не из предположений и догадок: "это проще и удобнее", "обычно в примерах (блогах, книгах) делают так", "я всегда так делал и всё было OK".
Ниже я постараюсь описать имеющиеся интерфейсы (API) для работы с метаданными, обозначить цели, которым они служат, и показать отличия (а они есть) в результатах использования различных интерфейсов.

В первую очередь хочется рассказать о CustomAttributeData, ибо он достаточно заметно отличается от других и далее нам будет совершенно не интересен. Отличается он прежде всего тем, что предназначен для использования в случаях, когда другие способы работы с метаданными недоступны, а именно для доступа к метаданным, загруженным в reflection-only context-е. Появился этот интерфейс позже остальных, с выходом второй версии .NET-фреймворка - именно тогда и появился reflection-only context. Подробнее о reflection-only context можно прочитать в статьях How to: Load Assemblies into the Reflection-Only Context и Reflection Only Assembly Loading, а мы про данное API на сегодня забудем.

Во вторую очередь необходимо рассказать о самом распространённом (и, чаще всего, не верном) способе доступа к метаданным - реализации упомянутого в самом начале интерфейса ICustomAttributeProvider. Это самый базовый, самый общий способ для доступа к метаданным. Можно сказать, "самый низкоуровневый" и, к сожалению так получилось, наверное, и самый удобный. Но давайте присмотримся внимательнее:

public interface ICustomAttributeProvider
{
  object[] GetCustomAttributes(bool inherit);
  object[] GetCustomAttributes(Type attributeType, bool inherit);
  bool IsDefined(Type attributeType, bool inherit);
}

ICustomAttributeProvider возвращает атрибуты как объекты типа object. Так сделано потому, что в .NET-фреймворк вообще нет ограничения на то, что в качестве атрибутов могут выступать исключительно наследники Attribute. Это ограничение накладывается CLS - Common Language Specification. А так как ICustomAttributeProvider "базовый способ для доступа к метаданным", как мы сказали, он обязан покрывать всевозможные ситуации и, значит, ничего "не знать" про Attribute. Отсюда, теоритически, напрашивается вывод, что нужно быть осторожным с таким вот кодом:

var attributes = (Attribute[])member.GetCustomAttributes(false);

Потому что приведение типов может выбросить исключение, если вдруг встретится не-CLS-compliant атрибут. На практике, конечно же, это не большая проблема, так как подобные атрибуты встречаются весьма и весьма редко. Более интересные сценарии, в которых результат, возвращаемый ICustomAttributeProvider, так же окажется не очень ожидаемым, мы разберём далее.

Третьим API для работы с метаданными в .NET-фреймворке является класс Attribute и его статические методы. Как уже отмечалось ранее, этот класс определяет метаданные, совместимые с Common Language Specification и это является новым уровнем абстракции, потому что позволяет задавать поведение, отличное от поведения атрибутов в базовом уровне. Поэтому именно это API является предпочтительным для получения метаданных в подавляющем большинстве случаев - тогда, когда вы ожидаете получить CLS-compliant-атрибуты, то есть атрибуты, тип которых является наследником Attribute. Но - обо всём по порядку.

Тип Attribute (сейчас будет не простое предложение!) обязывает каждый CLS-compliant-атрибут (то есть каждый свой наследник) снабдить атрибутом AttributeUsageAttribute, который имеет несколько свойств, интерес для нас сейчас из которых представляет Inherited. Это свойство влияет на поведение в обработке атрибута, поэтому при доступе к атрибутам посредством ICustomAttributeProvider (который "не знает про Attribute") будет получаться не верный, то есть не ожидаемый [скорее всего] результат. Об этом так прямо и заявлено в документации, например, к методу ICustomAttributeProvider::GetCustomAttributes(bool inherit):

Calling ICustomAttributeProvider.GetCustomAttributes on PropertyInfo or EventInfo when the inherit parameter of GetCustomAttributes is true does not walk the type hierarchy. Use System.Attribute to inherit custom attributes.

Сказывается это, например, в таком вот коде:

using System;

[AttributeUsage(AttributeTargets.All, Inherited = true)]
internal sealed class Test1Attribute : Attribute { }

[AttributeUsage(AttributeTargets.All, Inherited = true)]
internal sealed class Test2Attribute : Attribute { }

internal class Base
{
  [Test1]
  public virtual int Property { get; set; }
}

internal class Derived : Base
{
  [Test2]
  public override int Property { get; set; }
}

internal static class Program
{
  private static void Main() {
    var type = typeof(Derived);
    var property = type.GetProperty("Property");
    const bool Inherit = true;
    var reflectionAttributes = property.GetCustomAttributes(Inherit);
    var attributeAttributes = Attribute.GetCustomAttributes(property, typeof(Attribute), Inherit);
    PrintAttributes("Reflection", reflectionAttributes);
    PrintAttributes("Attribute", attributeAttributes);
  }

  private static void PrintAttributes<T>(string label, T[] attributes) {
    Console.WriteLine("Test: " + label);
    foreach(var item in attributes) {
      Console.WriteLine("  " + item.GetType().Name);
    }//for
  }
}

Результат:

Test: Reflection
  Test2
Test: Attribute
  Test2
  Test1

То есть, при использовании правильного API мы получили более ожидаемый результат. В поддержку именно данного способа доступа к метаданным высказывается даже MSDN в описании класса Attribute:

The Attribute class provides convenient methods to retrieve and test custom attributes. For more information about using attributes, see Applying Attributes and Extending Metadata Using Attributes.

И далее по ссылкам из цитаты в статье Retrieving Information Stored in Attributes.

Последний, четвёртый способ доступа к атрибутам - посредством TypeDescriptor:

var provider = TypeDescriptor.GetProvider(type);
var descriptor = provider.GetTypeDescriptor(type);
var properties = descriptor.GetProperties();
var typeDescriptorAttributes = properties["Property"].Attributes.Cast<Attribute>().ToArray();
PrintAttributes("TypeDescriptor", typeDescriptorAttributes);

Результат:

Test: TypeDescriptor
  __DynamicallyInvokable
  Serializable
  Test1
  ComVisible
  Test2
  CLSCompliant

Достаточно неожиданно? Да - мы не навешивали никаких атрибутов, а они "есть". TypeDescriptor, в отличии от описанных ранее механизмов, предоставляет расширяемый во время выполнения стандартный способ доступа к метаданным. Так же, TypeDescriptor умеет по особенному работать с некоторыми атрибутами, например, имеющими конструктор по-умолчанию (public-конструктор без параметров) и inherited-атрибуты "в нём работают" не только при переопределении (override) свойств, но и при объявлении в наследнике нового свойства с именем, идентичным свойству в базовом классе (при помощи модификатора new).

Предназначен TypeDescriptor для работы с компонентной моделью (как я это называю по имени пространства имён System.ComponentModel, в котором находится множество типов, "that are used to implement the run-time and design-time behavior of components and controls") метаданные для которой особенно важны (и в которой играют значительную роль). Поэтому TypeDescriptor работает с атрибутами не совсем так же, как ICustomAttributeProvider или Attribute - он вводит новое поведение для атрибутов, и при работе, например, с компонентами (классами, реализующими IComponent) в design-time предпочтительнее использовать именно его, а не первые три способа.

Подробнее останавливаться на особенностях TypeDescriptor я сейчас не буду - этот класс заслуживает отдельной статьи, даже отдельной главы книги. Отмечу лишь, что не смотря на свой высокий уровень абстракции, TypeDescriptor (то есть компонентная модель) тесно связан с типом Attribute: например, свойство Attribute::TypeId и метод Attribute::Match(object) используются, в первую очередь, там.

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

Update: В версии 4.5 фреймворка появился замечательный класс CustomAttributeExtensions с методами расширения к классам, реализующим ICustomAttributeProvider, вызывающие внутри себя "правильные" методы класса Attribute. Так что использовать правильный подход теперь так же просто, как и "привычный" :о)