Speak.Me Учить иностранные слова

Рефлексия (отражение, reflection) в C#

Программа компилируется в сборку, которая содержит метаданные, скомпилированный код и ресурсы. Инспектирование метаданных и скомпилированного кода во время выполнения называется рефлексией.

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

В пространстве имен System.Reflection и System.Reflection.Emitсодержится ряд классов для рефлексии.

Рефлексия типов и System.Type

Экземпляр System.Type представляет метаданные для типа.

Получение экземпляра Type

Получить экземпляр System.Type можно вызвав метод GetType на любом объекте или с помощью оператора typeof:

Экземпляр Type также можно извлечь по имени, вызвав метод GetType на объекте сборки (тип Assambly), содержащей данный тип, передав ему имя типа в виде строки:

Класс Assembly также содержит метод GetTypes, возвращающий все типы, содержащиеся в сборке:

Если объект Assambly отсутствует, тип можно получить через его имя с указанием сборки (полное имя типа, за которым следует полностью заданное имя сборки). При этом сборка неявно загружается, как если бы вызывался метод Assembly.Load(string):

С помощью свойств экземпляра System.Type можно получить доступ к имени типа, его сборке, базовому типу, его видимости и т.д.:

Получить тип массива можно также вызвав MakeArrayType на типе его элементов. Методу можно передать целочисленный аргумент, тогда он вернет тип многомерного прямоугольного массива с указанным числом измерений:

Метод GetElementType делает обратное — он извлекает тип элементов массива из экземпляра типа массива:

Метод GetArrayRank возвращает количество измерений в прямоугольном массиве:

Вложенные типы можно извлечь вызвав метод GetNestedTypes на содержащем их типе:

Имена типов

Тип имеет свойства Namespace, Name и FullName. Как правило FullName состоит из первых двух свойств, исключение составляют вложенные типы и закрытые обобщенные типы.

Также тип имеет свойство AssemblyQualifiedName, которое возвращает FullName и через запятую полное имя его сборки.

Для вложенных типов имя родительского типа присутствует только в FullName, имя родительского типа отделяется от имени вложенного символом +:

Имена обобщенных типов содержат суффикс состоящий из символа ', за которым следует количество параметров типа. Для несвязанных типов это правило применяется к Name и FullName, для связанных типов FullName дополняется перечислением имен всех параметров типа с указанием сборки:

Имена массивов и указателей также включают свои суффиксы:

Если экземпляр Type описывает параметр ref или out, то его имя снабжается суффиксом &:

Базовые типы и интерфейсы

Свойство BaseType возвращает базовый тип, а метод GetInterfaces — реализуемые интерфейсы:

Метод IsInstanceOfType возвращает true, если переданный ему объект является экземпляром типа, на котором он вызван. Метод аналогичен использованию оператораis:

Метод IsAssignableFrom возвращает true, если переданный ему тип реализует интерфейс или является производным классом от типа, на котором он вызван:

Метод IsSubclassOf работает также, но исключает интерфейсы.

Создание экземпляра типа

Существует два способа динамически создать объект из его типа:

  • вызов статического метода Activator.CreateInstance
  • вызов метода Invoke на экземпляре ConstructorInfo, который можно получить вызвав метод GetConstructor на экземпляре Type

Метод Activator.CreateInstance принимает экземпляр Type и дополнительные аргументы, передаваемые конструктору:

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

Метод Invoke более универсален, он может быть использован, когда значения аргументов не позволяют устранить неоднозначность между перегруженными конструкторами (например, если аргумент равен null):

Для доступа к не публичному конструктору необходимо дополнительно передать BindingFlags.

Для динамического создания делегата необходимо вызвать Delegate.CreateDelegate:

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

Вместо имени метода в CreateDelegate можно передавать MethodInfo.

Обобщенные типы

Type может представлять закрытый или несвязанный обобщенный тип. Как и на этапе компиляции экземпляр, экземпляр закрытого обобщенного типа может быть создан, несвязанного — нет:

Метод MakeGenericType преобразует несвязанный обобщенный в закрытый, принимая в качестве параметров желаемые аргументы типа:

Метод GetGenericTypeDefinition делает обратное:

Свойство IsGenericType возвращает true если тип является обобщенным, а свойство IsGenericTypeDefinition — если обобщенный тип является несвязанным:

Метод GetGenericArguments возвращает аргументы типа для закрытых обобщенных типов:

Для несвязанных обобщенных типов метод GetGenericArguments возвращает типы-заполнители, указанные при объявлении обобщенного типа:

Рефлексия членов типа

Метод GetMembers экземпляра Type возвращает члены типа. При вызове без аргументов метод возвращает все публичные члены типа и его базовых типов:

Методу GetMembers в качестве дополнительного параметра можно передатьenum MemberTypes, что ограничит возвращаемые им виды членов. MemberTypesвключает следующие члены: All, Custom, Field, NestedType,TypeInfoConstructor, Event, Method, Property. Также можно использовать специальные методы, возвращающие отдельные виды членов: GetMethods,GetFields, GetProperties, GetEvents, GetConstructors иGetNestedTypes.

Метод GetMember извлекает отдельный член по имени, но возвращает массив, т.к. члены могут быть перегружены:

Существуют также методы GetMethod, GetField, GetPropertie, GetEvent,GetConstructor и GetNestedType, возвращающие конкретный вид члена по имени.

TypeInfo

Члены типа можно также получить с помощью свойств класса TypeInfo, которые возвращают не массив, а IEnumerable<T>. Чаще других используется свойство DeclaredMembers, которое в отличает от метода GetMembers не возвращает унаследованные члены:

TypeInfo также содержит свойства для возврата специфических видов членов: DeclaredProperties, DeclaredMethods, DeclaredEvents и т.д., а также методы для возврата конкретных членов по имени, например, GetDeclaredMethod. Последний не может применяться для возврата перегруженных методов, т.к. нет способа указать тип параметров. Вместо этого можно выполнить запрос LINQ:

MemberInfo

Все указанные выше методы возвращают массив объектов MemberInfo, или его производных классов. Класс MemberInfo имеет свойство MemberTypes типа enum MemberTypes, описанного выше. Также объект MemberInfo имеет свойствоName и два свойства возвращающие Type:

  • DeclaringType — возвращает тип содержащий член
  • ReflectedType — возвращает тип, на котором был вызван GetMembers

Если метод GetMembers был вызван на производном типе, то для свойства, содержащегося в базовом классе DeclaringType вернет базовый тип, а ReflectedType — производный.

MemberInfo имеет также два свойства-идентификатора члена:

  • MethodHandle — уникально для каждого метода внутри домена приложения
  • MetadataToken — уникально по всем типам и членам в рамках модуля сборки

MemberInfo является абстрактным базовым классом для следующих типов:

  • FieldInfo
  • PropertyInfo
  • MethodBase
    • ConstructorInfo
    • MethodInfo
  • EventInfo
  • Type

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

Рефлексия специальных членов

Индексатор типа можно получить с помощью метода GetDefaultMembers, С точки зрения рефлексии он представляет собой свойство типа вида DefaultMember.

Член перечисления (enum) можно получить с помощью метода GetField. Члены перечисления с точки зрения рефлексии представляют собой статически поля перечисления.

Оператор можно получить с помощью того же метода GetMethod передав ему строку "op_" + название оператора. Операторы при рефлексии рассматриваются как статические методы со специальными именами, начинающимися с op_, например op_Addition.

Финализатор можно получить  с помощью GetMethod, передав ему в качестве названия метода строку "Finalize".

Свойства и события при рефлексии представляются несколькими объектами:

  • метаданные, представляющие свойство или событие (объект PropertyInfoили EventInfo)
  • один или два поддерживающих метода

Поддерживающие методы при написании программы инкапсулируются в соответствующее свойство или событие, но после компиляции они представляются как обычные методы, которые можно вызвать как и все остальные. GetMethodsвозвращает поддерживающие методы вместе со всеми остальными:

Идентифицировать эти методы можно через свойство IsSpecialNameобъекта MethodInfo: оно вернет true для методов доступа к свойствам, индексаторам, событиям и для операторов. Для обычных методов и финализатора (метод Finalize) оно вернет false.

Поддерживающие методы свойств имеют имена вида get_XXX и set_XXX (где XXX — название свойства), индексаторов — get_Item и set_Item, событий — add_XXXи remove_XXX.

Каждый поддерживающий метод имеет собственный связанный с ним объект MethodInfo, доступ к которому можно так:

Методы GetAddMethod и GetRemoveMethod делают тоже самое для EventInfo.

Динамический вызов члена

Получив объект MemberInfo, его можно динамически вызвать или извлечь/установить его значение. Это называется динамическим связыванием или поздним связыванием, т.к. оно выполняется на этапе выполнения программы, а не при компиляции.

Методы GetValue и SetValue извлекают и устанавливают значение PropertyInfoили FieldInfo. Первый аргумент — это экземпляр, который для статического члена равен null. Доступ к индексатору осуществляется как доступ к свойству с именем Item, а методам GetValue и SetValue вторым параметром надо передавать значение индексатора.

Для динамического вызова метода необходимо вызвать Invoke на объекте MethodInfo, передав ему массив аргументов для динамически вызываемого метода. Если хотя бы один аргумент будет иметь неверный тип, будет сгенерировано исключение (при выполнении).

Метод GetParameters, определенный в MethodBase (базовый класс для MethodInfo и
ConstructorInfo) возвращает метаданные параметров:

Чтобы передать параметры по ссылке (ref или out), перед получением метода необходимо вызвать MakeByRefType на типе:

Динамический вызов методов можно осуществлять с помощью делегатов, что значительно ускорит выполнение:

BindingFlags и доступ к непубличным членам

Всем методам Get*, указанным выше, можно дополнительно передатьenum BindingFlags, который служит своеобразным фильтром для извлекаемых метаданных и позволяет изменить стандартные критерии отбора. В частности с его помощью можно извлечь непубличные члены:

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

  • BindingFlags.Public | BindingFlags.Instance
  • BindingFlags.Public | BindingFlags.Static
  • BindingFlags.NonPublic | BindingFlags.Instance
  • BindingFlags.NonPublic | BindingFlags.Static

Флаг NonPublic включает internal, protected, protected internal иprivate.

Флаг DeclaredOnly ограничивает набор (в отличие от остальных флагов, которые его расширяют) и позволяет исключить методы, унаследованные от базовых классов, если только они не переопределены.

Обобщенные методы

Открытые обобщенные методы напрямую вызвать нельзя — будет сгенерировано исключение. Предварительно их необходимо закрыть, вызвав метод MakeGenericMethod на объекте MethodInfo, которому передаются конкретные значения для параметров типа. Метод вернет другой объект MethodInfo, который затем можно вызвать:

MethodBody

Для получения информации о содержимом метода необходимо вызвать GetMethodBody на объекте метод MethodBase. Метод вернет объект MethodBody, который имеет свойства для инспектирования локальных переменных метода, конструкций обработки исключений, размера стэке и кода IL.

Динамическая генерация кода

С помощью класса DynamicMethod из пространства имен System.Reflection.Emit можно генерировать методы налету:

Конструктор класса принимает четыре обязательных аргумента:

  • string — название метода
  • Type — возвращаемый методом тип (null в случае void)
  • Type[] — массив типов аргументов (null если метод не принимает аргументов)
  • Type — тип, к которому будет относиться метод

Класс OpCodes имеет статические поля (доступные только для чтения) для каждого кода операции IL. Такое поле передается методу Emit экземпляра ILGenerator для выполнения соответствующей операции. Большая часть функционала реализуется именно таким способом, хотя класс ILGenerator имеет специализированные методы для генерации меток, локальных переменных, обработки исключений.

Динамически генерируемый метод всегда заканчивается кодом операции Opcodes.Ret, что означает возврат из метода.

Метод EmitWriteLine экземпляра ILGenerator — это сокращение для вызова ряда кодов операций:

Конструктору DynamicMethod четвертым аргументом можно передать тот или иной тип, в результате чего создаваемый динамический метод получит доступ к непубличным методам данного типа:

Стэк оценки (Evaluation Stack)

Центральной концепцией в IL является стэк оценки: с его помощью можно передавать значения в методы, а также получать возвращаемые методом значения. Для того чтобы вызвать метод с аргументами, аргументы сначала необходимо поместить в стэк оценки. После этого вызывается метод, который по мере необходимости извлекает аргументы из стэка и помещает в него свой результат.

Код операции OpCodes.Ldstr позволяет поместить в стэк строку, коды OpCodes.Ldc* — числовые литералы различных типов и размеров:

Для сложения двух чисел их необходимо поместить в стэк, затем вызвать код Add, который извлечет числа, сложит их и поместит результат обратно в стэк:

Аналогично и с другими вычислениями:

Передача аргументов и возврат значения из динамического метода

Чтобы использовать аргументы, переданные динамическому методу, их также нужно загрузить в стэк оценки. Сделать это можно с помощью кодов Ldarg и Ldarg_XXX(XXX — номер аргумента). Возврат значений из динамического метода также осуществляется через стэк оценки. Для этого необходимо перед выходом из метода оставить в стэке ровно одно значение.

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

По завершении метода стэк должен иметь в точности 0 (если метод не возвращает значения) или 1 значение. Если нарушить это правило метод не будет выполнен. Удалить элемент из стэка без обработки можно кодом OpCodes.Pop.

Генерация локальных переменных

Объявить локальную переменную можно с помощью вызова метода DeclareLocal на экземпляре ILGenerator. Метод вернет объект LocalBuilder, который можно использовать в сочетании с кодами Ldloc — помещает значение переменной в стэк для дальнейшего использования, и Stloc — извлекает значение из стэка и сохраняет его в локальной переменной:

Ветвление

Циклы вроде while, do и for в IL не предусмотрены. Вместо этого ветвление выполняется с помощью меток и эквивалентов оператора goto. Имеются коды операций ветвления:

  • Br — безусловное ветвление
  • Brtrue — ветвление, если значение в стэке оценки равно true
  • Blt — ветвление, если первое значение в стэке меньше второго

Для установки цели ветвления сначала нужно вызвать DefineLabel (вернет объект Label), а затем вызвать MarkLabel в месте, где должна быть установлена метка:

Создание объектов и вызов их методов

Создать объект можно с помощью кода операции Newobj, которая вызовет конструктор и загрузит созданный экземпляр в стэк оценки:

После этого можно вызывать экземплярные методы объекта с помощью кодов Callили Callvirt:

Call используется для обращения к статическим методам (значимых и ссылочных типов) и экземплярным методам значимых типов, а Callvirt — для обращения к экземплярным методам ссылочных типов.

Имитирование сборок и типов

Класс DynamicMethod может генерировать только методы, а если необходимо сгенерировать, например, тип, схема усложняется. Тип не может существовать сам по себе: он должен находиться в модуле внутри сборки. Добавить тип в существующую сборку нельзя — сборка является неизменяемой после создания. Поэтому потребуется динамически создать новую сборку и модуль. Однако сборка необязательно должна присуствовать на диске, она может располагаться в памяти. Создать сборку и модуль можно с помощью классов AssemblyBuilder и ModuleBuilder:

Динамические сборки не подвержены сборке мусора и остаются в памяти до завершения домена приложения.

Имея модуль можно воспользоваться классом TypeBuilder для создания типа:

Перечисление TypeAttributes задает различные настройки для типа: оно имеет члены, соответствующие модификаторам доступа,  Abstract или Sealed — для объявления класса абстрактным или закрытым, Interface — для определения интерфейса, Serializable — эквивалентен применению атрибута[Serializable]Explicit — эквивалентен применению атрибута [StructLayout(LayoutKind.Explicit)].

Методу DefineType также можно передать необязательный атрибут, указывающий базовый класс:

  • для определения структуры нужно передать System.ValueType
  • для определения делегата — System.MulticastDelegate
  • для реализации интерфейсов — массив типов интерфейсов
  • для определиния интерфейса — TypeAttributes.Interface | TypeAttributes.Abstract

После создания типа можно создавать члены внутри него:

После определения членов можно завершить объявление типа:

А после создания типа можно создавать его экземпляры и вызывать его методы:

Созданную сборку можно сохранить в файл на диск с помощью метода Saveкласса AssemblyBuilder, которому нужно передать имя файла. Но предварительно нужно выполнить два действия:

  • при создании объекта AssemblyBuilder передать в конструктор флаг AssemblyBuilderAccess.Saveили AssemblyBuilderAccess.RunAndSave
  • при создании объекта ModuleBuilder указать имя файла

Можно также дополнительно установить свойства объекта AssemblyName, такие как Version или KeyPair:

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

После сохранения в файл динамическая сборка становится обычной сборкой.

Имитирование членов

Имитирование методов

Методы можно создавать не только с помощью класса DynamicMethod, но и с помощью класса DefineMethod. При вызове DefineMethod можно указывать возвращаемый тип и типы принимаемых параметров:

Вызов DefineParameter является необязательным. С его помощью можно присвоить параметру имя, что обычно имеет смысл при сохранении сборки на диск делая метод более дружественным для потребителя. Число 1 ссылается на первый параметр (0 соответствует возвращаемому значению). Метод DefineParameter возвращает объект ParameterBuilder, на котором можно вызвать SetCustomAttribute для присоединения атрибутов к параметру.

Для параметров передаваемых по ссылке необходимо вызвать MakeByRefType на типе параметра:

Параметры out определяются аналогично, за исключением того, что DefineParameter вызывается следующим образом:

Для генерации экземплярного метода, при вызове DefineMethod необходимо указать MethodAttributes.Instance:

В экземплярных методах нулевым аргументом по умолчанию является this, остальные аргументы нумеруются начиная c 1, поэтому Ldarg_0 загрузит в стэк оценки this, а Ldarg_1 — первый реальный аргумент метода.

При переопределении методов базового класса в производном полезно указыватьMethodAttributes.HideBySig. Он указывает на необходимость скрывать переопределяемый метод базового класса только если он полностью совпадает по сигнатуре. Без этого сокрытие основывается на имени.

Имитирование поле и свойств

Для создания поля необходимо вызвать DefineField на объекте TypeBuilder, передав ему имя поля, тип и видимость:

Создание свойств и индексаторов несколько сложнее. Сначала необходимо вызвать DefineProperty на объекте TypeBuilder, передав ему имя и тип свойства:

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

После этого необходимо создать методы доступа к свойству get и set. По соглашению они именуются также как свойство, но с префиксом get_ и set_. После создания методов их нужно присоединить к свойству с помощью вызова методов SetGetMethodи SetSetMethod на объекте PropertyBuilder:

События можно создавать аналогичным способом, вызывая метод DefineEvent на объекте TypeBuilder, а затем написав методы доступа и присоединив их к объекту EventBuilder с помощью вызова методов SetAddOnMethodи SetRemoveOnMethod.

Имитирование конструкторов

Определить конструктор можно с помощью вызова метода DefineConstructor на объекте TypeBuilder. Делать это необязательно — если не определен ни один конструктор будет использован стандартный конструктор без параметров. Для подтипа стандартный конструктор вызовет конструктор базового класса. При определении конструктора (или нескольких) стандартный конструктор устраняется.

При имитации типов конструктор является единственным местом для инициализации полей:

Конструктор базового класса по умолчанию не вызывается — он должен быть вызван явно в случае необходимости:

Присоединение атрибутов

Присоединить атрибут к динамически создаваемой конструкции можно с помощью метода SetCustomAttribute соответствующего объекта-билдера с передачей ему экземпляра CustomAttributeBuilder.

Например, чтобы присоединить к свойству или полю такой атрибут:

необходимо:

Имитирование обобщений

Обобщенные методы

Для генерации обобщенного метода необходимо:

  • вызвать DefineGenericParameters на объекте MethodBuilder для получения массива объектов GenericTypeParameterBuilder
  • вызвать SetSignature на объекте MethodBuilder, передав ему полученный в первом пункте массив параметров типа
  • назначить параметрам имена (необязательно)
Метод DefineGenericParameters может принимать любое количество строковых аргументов, которые соответствуют именам желаемых обобщенных типов.

Класс GenericTypeParameterBuilder позволяет указывать ограничение базового типа и ограничения интерфейсов:

Для других ограничений необходимо вызывать метод SetGenericParameterAttributes и передавать ему enum GenericParameterAttributes, который включает следующие значения:

  • DefaultConstructorConstraint
  • NotNullableValueTypeConstraint
  • ReferenceTypeConstraint
  • Covariant (эквивалент модификатора out)
  • Contravariant (эквивалент модификатора in)

 Обобщенные типы

Обобщенные типы определяются аналогично, только метод DefineGenericParameters вызывается на объекте TypeBuilder: