Очередной пост из серии "Что такое хорошо и что такое плохо?". В этот раз о том, как правильно извлекать .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. Это самый базовый, самый общий способ для доступа к метаданным. Можно сказать, "самый низкоуровневый" и, к сожалению так получилось, наверное, и самый удобный. Но давайте присмотримся внимательнее:
{
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. Отсюда, теоритически, напрашивается вывод, что нужно быть осторожным с таким вот кодом:
Потому что приведение типов может выбросить исключение, если вдруг встретится не-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.
Сказывается это, например, в таком вот коде:
[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
}
}
Результат:
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 descriptor = provider.GetTypeDescriptor(type);
var properties = descriptor.GetProperties();
var typeDescriptorAttributes = properties["Property"].Attributes.Cast<Attribute>().ToArray();
PrintAttributes("TypeDescriptor", typeDescriptorAttributes);
Результат:
__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. Так что использовать правильный подход теперь так же просто, как и "привычный" :о)
Комментариев нет:
Отправить комментарий