Главная страница   /   2.2. Чтение метаданных и выполнение кода (Метапрограммирование в .NET

Метапрограммирование в .NET

Метапрограммирование в .NET

Кевин Хазард

2.2. Чтение метаданных и выполнение кода

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

Примечание

Не забудьте включать using System.Reflection; в любой кодовый файл, который использует рефлексию.

Получение начальной точки

В зависимости от того, что вы ищете, есть две основные точки вступления в Reflection API: сборка или тип. Чтобы получить ссылку на тип, все, что вам нужно, это его имя. Вот как вы получите тип Random:

var type = Type.GetType("System.Random");

Все, что нужно сделать, чтобы получить ссылку Type, это указать полное имя типа. Если вы используете только Random в качестве имени, вы получите null ссылку в качестве возвращаемого значения.

Есть шесть перегруженных версий GetType() ... и это то, к чему вам нужно привыкнуть, когда вы работаете с Reflection API. Большинство методов Get, с которыми вы встретитесь, имеют ряд перегруженных вариантов. Как правило, есть больше чем один способ решить проблему, и рассмотреть, какие перегруженные варианты доступны, стоит того. Например, GetType() не выбросит исключение, если он не сможет найти тип в текущей сборке или любой связанной сборке, но вы можете изменить это с помощью перегруженного варианта:

var type = Type.GetType("System.Random", true);

Мы также можем получить тип при помощи ключевого слова typeof:

var type = typeof(Random);

Наконец, каждый объект имеет метод GetType(), как он определен в классе Object:

var type = new Random().GetType();

Последние два подхода являются более безопасными, чем тот, который основан на строках. С typeof вы будете знать во время компиляции, правильный ли ваш код, а вызов GetType() для объекта гарантирует ненулевое возвращаемое значение. Использование строк обеспечивает большую гибкость, но тут легко сделать опечатку и не знать об этом до момента выполнения. Хорошие модульные тесты смогут отсеять такого рода ошибки, но не впадайте в ложное чувство безопасности, если ваш основанный на рефлексии код компилируется. Не забудьте протестировать его!

Другой подход заключается в том, чтобы загрузить сборку, а затем копаться в ее содержании. Класс Assembly имеет ряд методов load, чтобы сделать это. При загрузке сборки вы загружаете ее содержимое в текущий AppDomain, и он может быть использован вашим кодом, основанным на рефлексии. При ссылке на сборку в Visual Studio или в csc.exe, эти ссылки остаются в сборке и автоматически загружаются CLR.

Понимание AppDomain

Об AppDomain можно думать как об изолированной области, где выполняется код. Большую часть времени вам не придется иметь дело с AppDomain, поскольку во время выполнения он устанавливается для вас при запуске приложения. Создание нескольких AppDomain в приложении является возможным, но это выходит за рамки того, что мы здесь рассматриваем.

Вот три примера загрузки сборок, которые производят те же результаты (с несколько иной реализацией):

var assembly = Assembly.Load(new AssemblyName()
	{ Name = "mscorlib", Version = new Version(4, 0, 0, 0) });
var assembly2 = Assembly.Load("mscorlib, Version=4.0.0.0");
var assembly3 = Assembly.LoadFrom(
	@"file:///C:/Windows/Microsoft.NET/Framework/v4.0.30319/mscorlib.dll");

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

Примечание

Хотя все методы Load() дадут вам ссылку на сборку, на самом деле они работают по-другому.

Вы также можете получить ссылку на сборку, которая выполняется в данный момент, через GetExecutingAssembly(), а на сборку, с которой все началось при помощи метода точки входа (например, Main()) через GetEntryAssembly(). Наконец, вы можете получить ссылку на сборку, содержащую данный тип, при помощи свойства Assembly, и вы можете найти тип в сборке при помощи GetType():

var randomAssembly = typeof(Assembly).Assembly;
var randomType = randomAssembly.GetType("System.Random");

Не-дженерик тип, как Random, легко получить, а как насчет попытки найти класс Lazy<T> в mscorlib?

var lazyType = randomAssembly.GetType("System.Lazy`1");

Имена дженерик классов и методов используют этот формат с символом ` (tick format), где число после ` определяет количество дженерик параметров, которые принимает универсальный класс или метод. Можно также получить дженерик тип, используя ключевое слово typeof:

var lazyType = typeof(Lazy<>);

Если у типа есть несколько дженерик аргументов, вы можете использовать серию запятых, чтобы указать число аргументов. Например, следующий код получает Tuple <T1,T2,T3>:

var threeTupleType = typeof(Tuple<,,>);

Идея infoof

Ключевое слово typeof является единственным в своем роде в C#. Там нет methodof, fieldof и так далее. Эти другие мифические операторы называются infoof (сочетание info и of). Есть хорошая статья (Eric Lippert (blog), “In Foof We Trust: A Dialogue,” http://mng.bz/k0eQ), которая объясняет, почему их нет в C#, другая статья (Patrick Smacchia (posted by), “Elegant infoof operators in C# (read Info Of),” June 28, 2010, http://mng.bz/YK8h) показывает, как можно использовать деревья выражений (которые мы рассмотрим в главе 6), чтобы (почти) достичь того же результата.

Нахождение информации о членах

Как вы видели в предыдущем разделе, вы можете использовать GetType() для объекта Assembly, чтобы получить конкретный тип. Этот паттерн (например, использование метода Get) повторяется во всем Reflection API. Предположим, что вы хотите получить метод "Next" из типа Random. В этом случае, вы можете использовать GetMethod():

var randomType = new Random().GetType();
var nextMethod = randomType.GetMethod("Next");

Тем не менее, методы определения класса могут быть перегружены, так что вы захотите быть более точными с GetMethod() для получения определенного вида метода. На самом деле, если вы не дадите GetMethod() достаточно критериев, вы получите AmbiguousMatchException. Этот фрагмент кода получает метод "Next", который принимает два аргумента: минимальное и максимальное значение (оба int):

var nextWithTwoArguments = randomType.GetMethod("Next",
	new Type[] { typeof(int), typeof(int) });

Также можно различать методы, используя комбинацию из BindingFlags:

var nextWithTwoArguments = randomType.GetMethod("Next",
	BindingFlags.Instance | BindingFlags.Public,
	null, new Type[] { typeof(int), typeof(int) }, null);

Как вы видите, есть значение Public, которое можно использовать только для поиска открытых членов. Также есть значение NonPublic, которое обозначает, что вы можете видеть закрытый и защищенный (private и protected) контент с помощью Reflection API. В зависимости от вашей точки зрения, это может показаться серьезным нарушением безопасности, если вы считаете, что это позволяет произвольному коду менять значения закрытых полей в объекте. Но только «привилегированный» (privileged) код может использовать вызовы рефлексии. Если у вас нет этого уровня доступа, вам не удастся использовать рефлексию. В статье "Security Considerations for Reflection" на http://mng.bz/Gwau объясняются правила безопасности, касающиеся рефлексии.

Как только у вас есть метод, вы можете получить все его параметры через GetParameters():

var nextParameter = nextWithTwoArguments.GetParameters();

Это возвращает массив объектов ParameterInfo, который содержит имя, позицию и тип параметра.

Подобные методы существуют для свойств (GetProperty()), полей (GetField()) и событий (GetEvent()). У них также есть варианты для множественного числа, чтобы получить список членов, например, GetFields() возвращает массив объектов FieldInfo. Если вы также предположили, что Info находится в имени класса для всех результатов запроса c рефлексией, вы правы. Есть MethodInfo, FieldInfo, EventInfo и так далее.

Если метод или класс является дженерик, то, как они определяются и извлекаются с помощью Reflection API, может показаться немного запутанным. Как вы видели в предыдущем разделе, можно использовать «скобочный» синтаксис, чтобы получить дженерик тип в typeof. Но если вы знаете, какой тип вы хотите задать в Lazy<T>, вы можете сделать это вот так:

var openLazyType = typeof(Lazy<>);
var closedLazyType = typeof(Lazy<int>);

Первый способ дает открытый дженерик тип, потому что не все дженерик значения были указаны. Второй определяет значение для Т, так что это закрытый дженерик тип. Если у вас есть тип, и вы понятия не имеете, является ли он дженерик, используйте IsGenericType. Но если вы хотите знать, является ли он открытым или закрытым, используйте IsGenericTypeDefinition, который возвращает true, если текущий тип является открытым дженериком. Вы можете сделать закрытый дженерик тип из открытого с помощью MakeGenericType():

var openLazyType = typeof(Lazy<>);
var closedLazyType = openLazyType.MakeGenericType(typeof(int));

Для Type есть также метод GetGenericArguments(), который возвращает массив объектов Type, если вы хотите знать о дженерик значениях.

Сбор данных атрибутов

Это одно дело – иметь возможность получить информацию о членах из сборки, но интересная работа с рефлексией заключается в том, чтобы делать что-то с этими обнаруженными членами, как то вызов метода или добавление в класс пользовательской информации. Выполнение этих действий обычно требует добавления дополнительной информации к члену, так чтобы код рефлексии знал, что делать с этим членом. Во фреймворке модульного тестирования, как MSTest, вы отмечаете методы, которые должны быть запущены как модульный тест, при помощи TestMethodAttribute. В WCF вы можете использовать KnownTypeAttribute, чтобы указать, какие сообщения могут быть использованы в сценариях сериализации для других типов сообщений. Эти пользовательские атрибуты (которые наследуются от класса Attribute) хранятся в виде пользовательских метаданных в сборке, а Reflection API позволяет легко найти эту информацию.

Почти каждый класс в Reflection API наследуется от MemberInfo, который реализует интерфейс ICustomAttributeProvider. Этот интерфейс определяет два перегруженных метода GetCustomAttributes(), которые позволяют запросить член, чтобы увидеть, содержит ли он атрибут, который вы ищете:

var testAttribute = someMethod.GetCustomAttributes(
	typeof(TestMethodAttribute), true);

К сожалению, результат GetCustomAttributes() является массивом объектов, так что вам придется привести результаты к типу атрибута, который вы ищете. Вы можете использовать IsDefined(), чтобы убедиться, что атрибут находится на месте, прежде чем извлекать данные атрибута:

if(someMethod.IsDefined(typeof(TestMethodAttribute), true))

Вы можете создать дженерик метод расширения, чтобы скрыть некоторый беспорядок с приведением типов, с которым вы столкнетесь, используя GetCustomAttributes(), но есть также метод, называемый GetCustomAttributeData(), который возвращает список объектов CustomAttributeData. Эти объекты содержат данные атрибутов, разделенных между конструктором и именованными аргументами, а также тип атрибута (через свойство DeclaredType для значения Constructor), что позволяет получить данные атрибута и сам атрибут без какого-либо приведения. Следующее выражение LINQ получает все тестовые методы из определенной сборки:

var tests = from type in assemblyWithTests.GetTypes()
	from method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance)
	from attributeData in method.GetCustomAttributesData()
	where attributeData.Constructor.DeclaringType == typeof(TestMethodAttribute)
	select method;

Выполнение кода

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

Если у вас есть тип и вы хотите создать его экземпляр, не ищите дальше, чем Activator.CreateInstance():

var lazyIntType = typeof(Lazy<int>);
var lazyInt = Activator.CreateInstance(lazyIntType);

В данном случае CreateInstance() ищет открытый конструктор без аргументов для приведенного типа, вызывает этот конструктор и возвращает объект, типизированный как объект. Если вы не планируете использовать результат для других вызовов Reflection API, то это для вас будет довольно бесполезным. Вы можете привести возвращенный результат, если вы знаете, какой будет тип:

var lazyInt = Activator.CreateInstance(lazyIntType) as Lazy<int>;

Если вы создаете объекты, которые, как вы знаете, реализуют конкретный интерфейс или у них есть базовый класс где-то в иерархии наследования, это реалистичный подход. Вы также можете использовать дженерик вариант CreateInstance():

var lazyInt = Activator.CreateInstance<Lazy<int>>();

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

Вы не ограничены вызовом только открытого конструктора без аргументов для типа. На самом деле, это не редкость – искать классы, которые не имеют такого конструктора. Если вам нужно создать Lazy<int>, который имеет метод фабрики, вы можете сделать это:

var lazyInt = Activator.CreateInstance(lazyIntType,
	new Func<int>(() => { return new Random().Next(); } )) as Lazy<int>;

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

Если вы хотите вызвать метод для объекта посредством рефлексии, все что вам нужно, это объект MethodBase. Это базовый класс для ConstructorInfo (возвращаемого в результате вызова GetConstructor()) и MethodInfo (возвращаемого в результате вызова GetMethod()). MethodBase определяет метод Invoke() с набором переопределенных вариантов, два из которых мы рассмотрим. Первый принимает массив объектов, который связывается с аргументами конструктора. Следующий листинг показывает, как создать Lazy<int> через объект ConstructorInfo. Возвращаемое значение от Invoke() является новым объектом.

Листинг 2-1: Создание объекта через ConstructorInfo
var lazyIntType = typeof(Lazy<int>);
var lazyConstructor = lazyIntType.GetConstructor(new Type[] { typeof(Func<int>) });
var lazyInt = lazyConstructor.Invoke(new object[] {
	new Func<int>(() => { return new Random().Next(); } ) }) as Lazy<int>;
Console.Out.WriteLine(lazyInt.Value);

Другой Invoke() это то, что вы используете, чтобы вызвать метод для класса или объекта. Первым аргументом является объект, для которого вы хотите вызвать метод (или null, если метод является статическим). Вторым аргументом является массив объектов, который содержит все аргументы метода. В следующем листинге показано, как динамически вызвать метод Next() для объекта Random, чтобы получить значение от 0 до 9.

Листинг 2-2: Вызов метода для объекта
var randomType = typeof(Random);
var nextMethod = randomType.GetMethod("Next", new [] { typeof(int), typeof(int) });
var random = Activator.CreateInstance(randomType);
Console.Out.WriteLine(nextMethod.Invoke(random, new object[] { 0, 10 }));

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