Содержание
Программа компилируется в сборку, которая содержит метаданные, скомпилированный код и ресурсы. Инспектирование метаданных и скомпилированного кода во время выполнения называется рефлексией.
Скомпилированный код в сборке включает почти все содержимое изначального исходного кода, за исключением некоторой информации, которая теряется после компиляции: имена локальных переменных, комментарии, директивы препроцессора и др. Ко всему остальному можно получить доступ с помощью рефлексии.
В пространстве имен System.Reflection
и System.Reflection.Emit
содержится ряд классов для рефлексии.
Рефлексия типов и System.Type
Экземпляр System.Type
представляет метаданные для типа.
Получение экземпляра Type
Получить экземпляр System.Type
можно вызвав метод GetType
на любом объекте или с помощью оператора typeof
:
1 2 3 4 5 6 | Type t1 = DateTime.Now.GetType(); // Тип определяется при выполнении Type t2 = typeof (DateTime); // Тип определяется при компиляции Type t3 = typeof (DateTime[]); Type t4 = typeof (DateTime[,]); Type t5 = typeof (Dictionary<int,int>); Type t6 = typeof (Dictionary<,>); |
Экземпляр Type
также можно извлечь по имени, вызвав метод GetType
на объекте сборки (тип Assambly
), содержащей данный тип, передав ему имя типа в виде строки:
1 | Type t = Assembly.GetExecutingAssembly().GetType ("Demos.TestProgram"); |
Класс Assembly
также содержит метод GetTypes
, возвращающий все типы, содержащиеся в сборке:
1 2 3 | Assembly a = Assembly.LoadFrom (@"e:\demo\mylib.dll"); foreach (Type t in a.GetTypes()) Console.WriteLine (t); |
Если объект Assambly
отсутствует, тип можно получить через его имя с указанием сборки (полное имя типа, за которым следует полностью заданное имя сборки). При этом сборка неявно загружается, как если бы вызывался метод Assembly.Load(string)
:
1 2 | Type t = Type.GetType ("System.Int32, mscorlib, Version=2.0.0.0, " + "Culture=neutral, PublicKeyToken=b77a5c561934e089"); |
С помощью свойств экземпляра System.Type
можно получить доступ к имени типа, его сборке, базовому типу, его видимости и т.д.:
1 2 3 4 5 | Type stringType = typeof (string); string name = stringType.Name; Type baseType = stringType.BaseType; Assembly assem = stringType.Assembly; bool isPublic = stringType.IsPublic; |
Получить тип массива можно также вызвав MakeArrayType
на типе его элементов. Методу можно передать целочисленный аргумент, тогда он вернет тип многомерного прямоугольного массива с указанным числом измерений:
1 2 3 4 | Type simpleArrayType = typeof(int).MakeArrayType(); Console.WriteLine (simpleArrayType == typeof (int[])); // True Type cubeType = typeof(int).MakeArrayType (3); Console.WriteLine (cubeType == typeof (int[,,])); // True |
Метод GetElementType
делает обратное — он извлекает тип элементов массива из экземпляра типа массива:
1 | Type e = typeof (int[]).GetElementType(); // e == typeof (int) |
Метод GetArrayRank
возвращает количество измерений в прямоугольном массиве:
1 | int rank = typeof (int[,,]).GetArrayRank(); // 3 |
Вложенные типы можно извлечь вызвав метод GetNestedTypes
на содержащем их типе:
1 2 | foreach (Type t in typeof (System.Environment).GetNestedTypes()) Console.WriteLine (t.FullName); |
Имена типов
Тип имеет свойства Namespace
, Name
и FullName
. Как правило FullName
состоит из первых двух свойств, исключение составляют вложенные типы и закрытые обобщенные типы.
1 2 3 4 | Type t = typeof (System.Text.StringBuilder); Console.WriteLine (t.Namespace); // System.Text Console.WriteLine (t.Name); // StringBuilder Console.WriteLine (t.FullName); // System.Text.StringBuilder |
Также тип имеет свойство AssemblyQualifiedName
, которое возвращает FullName
и через запятую полное имя его сборки.
Для вложенных типов имя родительского типа присутствует только в FullName
, имя родительского типа отделяется от имени вложенного символом +
:
1 2 3 4 | Type t = typeof (System.Environment.SpecialFolder); Console.WriteLine (t.Namespace); // System Console.WriteLine (t.Name); // SpecialFolder Console.WriteLine (t.FullName); // System.Environment+SpecialFolder |
Имена обобщенных типов содержат суффикс состоящий из символа '
, за которым следует количество параметров типа. Для несвязанных типов это правило применяется к Name
и FullName
, для связанных типов FullName
дополняется перечислением имен всех параметров типа с указанием сборки:
1 2 3 4 5 6 7 8 9 | Type t = typeof (Dictionary<,>); Console.WriteLine (t.Name); // Dictionary'2 Console.WriteLine (t.FullName); // System.Collections.Generic.Dictionary'2 Console.WriteLine (typeof (Dictionary<int,string>).FullName); // System.Collections.Generic.Dictionary'2[[System.Int32, mscorlib, // Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089], // [System.String, mscorlib, Version=2.0.0.0, Culture=neutral, // PublicKeyToken=b77a5c561934e089]] |
Имена массивов и указателей также включают свои суффиксы:
1 2 3 4 | Console.WriteLine (typeof ( int[] ).Name); // Int32[] Console.WriteLine (typeof ( int[,] ).Name); // Int32[,] Console.WriteLine (typeof ( int[,] ).FullName); // System.Int32[,] Console.WriteLine (typeof (byte*).Name); // Byte* |
Если экземпляр Type
описывает параметр ref
или out
, то его имя снабжается суффиксом &
:
1 2 | Type t = typeof (bool).GetMethod ("TryParse").GetParameters()[1].ParameterType; Console.WriteLine (t.Name); // Boolean& |
Базовые типы и интерфейсы
Свойство BaseType
возвращает базовый тип, а метод GetInterfaces
— реализуемые интерфейсы:
1 2 3 4 5 6 7 8 9 10 | Type base1 = typeof (System.String).BaseType; Type base2 = typeof (System.IO.FileStream).BaseType; Console.WriteLine (base1.Name); // Object Console.WriteLine (base2.Name); // Stream foreach (Type iType in typeof (Guid).GetInterfaces()) Console.WriteLine (iType.Name); /* IFormattable IComparable IComparable'1 IEquatable'1 */ |
Метод IsInstanceOfType
возвращает true
, если переданный ему объект является экземпляром типа, на котором он вызван. Метод аналогичен использованию оператораis
:
1 2 3 4 | object obj = Guid.NewGuid(); Type target = typeof (IFormattable); bool isTrue = obj is IFormattable; bool alsoTrue = target.IsInstanceOfType (obj); |
Метод IsAssignableFrom
возвращает true
, если переданный ему тип реализует интерфейс или является производным классом от типа, на котором он вызван:
1 2 | Type target = typeof (IComparable), source = typeof (string); Console.WriteLine (target.IsAssignableFrom (source)); // True |
Метод IsSubclassOf
работает также, но исключает интерфейсы.
Создание экземпляра типа
Существует два способа динамически создать объект из его типа:
- вызов статического метода
Activator.CreateInstance
- вызов метода
Invoke
на экземпляреConstructorInfo
, который можно получить вызвав методGetConstructor
на экземпляреType
Метод Activator.CreateInstance
принимает экземпляр Type
и дополнительные аргументы, передаваемые конструктору:
1 2 | int i = (int) Activator.CreateInstance (typeof (int)); DateTime dt = (DateTime) Activator.CreateInstance (typeof (DateTime), 2000, 1, 1); |
Помимо этого методу можно передать рад дополнительных параметров, таких как сборка, из которой должен загружаться тип, целевой домен приложения и необходимость привязки к непубличному конструктору. Если подходящего конструктора найти не удается, генерируется исключение MissingMethodException
.
Метод Invoke
более универсален, он может быть использован, когда значения аргументов не позволяют устранить неоднозначность между перегруженными конструкторами (например, если аргумент равен null
):
1 2 3 4 5 | // Извлечение конструктора, который принимает одиночный параметр string ConstructorInfo ci = typeof (X).GetConstructor (new[] { typeof (string) }); // Создание объекта с помощью этого конструктора, передав ему null object foo = ci.Invoke (new object[] { null }); |
Для доступа к не публичному конструктору необходимо дополнительно передать BindingFlags
.
Для динамического создания делегата необходимо вызвать Delegate.CreateDelegate
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class Program { delegate int IntFunc (int x); static int Square (int x) { return x * x; } // Static method int Cube (int x) { return x * x * x; } // Instance method static void Main() { Delegate staticD = Delegate.CreateDelegate (typeof (IntFunc), typeof (Program), Square"); Delegate instanceD = Delegate.CreateDelegate (typeof (IntFunc), new Program(), "Cube"); Console.WriteLine (staticD.DynamicInvoke (3)); // 9 Console.WriteLine (instanceD.DynamicInvoke (3)); // 27 } } |
К созданному таким образом делегату можно обратиться вызвав DynamicInvoke
(как в примере выше), либо приведя его к типизированному делегату:
1 2 | IntFunc f = (IntFunc) staticD; Console.WriteLine (f(3)); |
Вместо имени метода в CreateDelegate
можно передавать MethodInfo
.
Обобщенные типы
Type
может представлять закрытый или несвязанный обобщенный тип. Как и на этапе компиляции экземпляр, экземпляр закрытого обобщенного типа может быть создан, несвязанного — нет:
1 2 3 4 | Type closed = typeof (List<int>); List<int> list = (List<int>) Activator.CreateInstance (closed); // OK Type unbound = typeof (List<>); object anError = Activator.CreateInstance (unbound); // Ошибка при выполнении |
Метод MakeGenericType
преобразует несвязанный обобщенный в закрытый, принимая в качестве параметров желаемые аргументы типа:
1 2 | Type unbound = typeof (List<>); Type closed = unbound.MakeGenericType (typeof (int)); |
Метод GetGenericTypeDefinition
делает обратное:
1 | Type unbound2 = closed.GetGenericTypeDefinition(); // unbound == unbound2 |
Свойство IsGenericType
возвращает true
если тип является обобщенным, а свойство IsGenericTypeDefinition
— если обобщенный тип является несвязанным:
1 2 3 4 | Type nullable = typeof (bool?); Console.WriteLine ( nullable.IsGenericType && nullable.GetGenericTypeDefinition() == typeof (Nullable<>)); // True |
Метод GetGenericArguments
возвращает аргументы типа для закрытых обобщенных типов:
1 2 | Console.WriteLine (closed.GetGenericArguments()[0]); // System.Int32 Console.WriteLine (nullable.GetGenericArguments()[0]); // System.Boolean |
Для несвязанных обобщенных типов метод GetGenericArguments
возвращает типы-заполнители, указанные при объявлении обобщенного типа:
1 | Console.WriteLine (unbound.GetGenericArguments()[0]); // T |
Рефлексия членов типа
Метод GetMembers
экземпляра Type
возвращает члены типа. При вызове без аргументов метод возвращает все публичные члены типа и его базовых типов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class Walnut { private bool cracked; public void Crack() { cracked = true; } } MemberInfo[] members = typeof (Walnut).GetMembers(); foreach (MemberInfo m in members) Console.WriteLine (m); /* Void Crack() System.Type GetType() System.String ToString() Boolean Equals(System.Object) Int32 GetHashCode() Void .ctor() */ |
Методу GetMembers
в качестве дополнительного параметра можно передатьenum MemberTypes
, что ограничит возвращаемые им виды членов. MemberTypes
включает следующие члены: All
, Custom
, Field
, NestedType
,TypeInfo
, Constructor
, Event
, Method
, Property
. Также можно использовать специальные методы, возвращающие отдельные виды членов: GetMethods
,GetFields
, GetProperties
, GetEvents
, GetConstructors
иGetNestedTypes
.
Метод GetMember
извлекает отдельный член по имени, но возвращает массив, т.к. члены могут быть перегружены:
1 2 | MemberInfo[] m = typeof (Walnut).GetMember ("Crack"); Console.WriteLine (m[0]); // Void Crack() |
Существуют также методы GetMethod
, GetField
, GetPropertie
, GetEvent
,GetConstructor
и GetNestedType
, возвращающие конкретный вид члена по имени.
TypeInfo
Члены типа можно также получить с помощью свойств класса TypeInfo
, которые возвращают не массив, а IEnumerable<T>
. Чаще других используется свойство DeclaredMembers
, которое в отличает от метода GetMembers
не возвращает унаследованные члены:
1 2 3 4 | IEnumerable<MemberInfo> members = typeof(Walnut).GetTypeInfo().DeclaredMembers; /* Void Crack() Void .ctor() Boolean cracked */ |
TypeInfo
также содержит свойства для возврата специфических видов членов: DeclaredProperties
, DeclaredMethods
, DeclaredEvents
и т.д., а также методы для возврата конкретных членов по имени, например, GetDeclaredMethod
. Последний не может применяться для возврата перегруженных методов, т.к. нет способа указать тип параметров. Вместо этого можно выполнить запрос LINQ:
1 2 3 | MethodInfo method = typeof (int).GetTypeInfo().DeclaredMethods .FirstOrDefault (m => m.Name == "ToString" && m.GetParameters().Length == 0); |
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
Его можно приводить к своим подтипам. Каждый подкласс имеет множество свойств и методов, отражающих все аспекты метаданных члена: его видимость, модификаторы, аргументы типа, параметры, возвращаемый тип и специальные атрибуты:
1 2 3 | MethodInfo m = typeof (Walnut).GetMethod ("Crack"); Console.WriteLine (m); // Void Crack() Console.WriteLine (m.ReturnType); // System.Void |
Рефлексия специальных членов
Индексатор типа можно получить с помощью метода GetDefaultMembers
, С точки зрения рефлексии он представляет собой свойство типа вида DefaultMember
.
Член перечисления (enum
) можно получить с помощью метода GetField
. Члены перечисления с точки зрения рефлексии представляют собой статически поля перечисления.
Оператор можно получить с помощью того же метода GetMethod
передав ему строку "op_"
+ название оператора. Операторы при рефлексии рассматриваются как статические методы со специальными именами, начинающимися с op_
, например op_Addition
.
Финализатор можно получить с помощью GetMethod
, передав ему в качестве названия метода строку "Finalize"
.
Свойства и события при рефлексии представляются несколькими объектами:
- метаданные, представляющие свойство или событие (объект
PropertyInfo
илиEventInfo
) - один или два поддерживающих метода
Поддерживающие методы при написании программы инкапсулируются в соответствующее свойство или событие, но после компиляции они представляются как обычные методы, которые можно вызвать как и все остальные. GetMethods
возвращает поддерживающие методы вместе со всеми остальными:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class Test { public int X { get { return 0; } set {} } } void Demo() { foreach (MethodInfo mi in typeof (Test).GetMethods()) Console.Write (mi.Name + " "); } /* get_X set_X GetType ToString Equals GetHashCode */ |
Идентифицировать эти методы можно через свойство IsSpecialName
объекта MethodInfo
: оно вернет true
для методов доступа к свойствам, индексаторам, событиям и для операторов. Для обычных методов и финализатора (метод Finalize
) оно вернет false
.
Поддерживающие методы свойств имеют имена вида get_XXX
и set_XXX
(где XXX
— название свойства), индексаторов — get_Item
и set_Item
, событий — add_XXX
и remove_XXX
.
Каждый поддерживающий метод имеет собственный связанный с ним объект MethodInfo
, доступ к которому можно так:
1 2 3 4 | PropertyInfo pi = typeof (Console).GetProperty ("Title"); MethodInfo getter = pi.GetGetMethod(); // get_Title MethodInfo setter = pi.GetSetMethod(); // set_Title MethodInfo[] both = pi.GetAccessors(); // Length==2 |
Методы GetAddMethod
и GetRemoveMethod
делают тоже самое для EventInfo
.
Динамический вызов члена
Получив объект MemberInfo
, его можно динамически вызвать или извлечь/установить его значение. Это называется динамическим связыванием или поздним связыванием, т.к. оно выполняется на этапе выполнения программы, а не при компиляции.
1 2 3 | object s = "Hello"; PropertyInfo prop = s.GetType().GetProperty ("Length"); int length = (int) prop.GetValue (s, null); // 5 |
Методы GetValue
и SetValue
извлекают и устанавливают значение PropertyInfo
или FieldInfo
. Первый аргумент — это экземпляр, который для статического члена равен null
. Доступ к индексатору осуществляется как доступ к свойству с именем Item
, а методам GetValue
и SetValue
вторым параметром надо передавать значение индексатора.
Для динамического вызова метода необходимо вызвать Invoke
на объекте MethodInfo
, передав ему массив аргументов для динамически вызываемого метода. Если хотя бы один аргумент будет иметь неверный тип, будет сгенерировано исключение (при выполнении).
1 2 3 4 5 6 | Type type = typeof (string); Type[] parameterTypes = { typeof (int) }; MethodInfo method = type.GetMethod ("Substring", parameterTypes); object[] arguments = { 2 }; object returnValue = method.Invoke ("stamp", arguments); Console.WriteLine (returnValue); // "amp" |
Метод GetParameters
, определенный в MethodBase
(базовый класс для MethodInfo
и
ConstructorInfo
) возвращает метаданные параметров:
1 2 3 4 5 6 | ParameterInfo[] paramList = method.GetParameters(); foreach (ParameterInfo x in paramList) { Console.WriteLine (x.Name); // startIndex Console.WriteLine (x.ParameterType); // System.Int32 } |
Чтобы передать параметры по ссылке (ref
или out
), перед получением метода необходимо вызвать MakeByRefType
на типе:
1 2 3 4 5 | object[] args = { "23", 0 }; Type[] argTypes = { typeof (string), typeof (int).MakeByRefType() }; MethodInfo tryParse = typeof (int).GetMethod ("TryParse", argTypes); bool successfulParse = (bool) tryParse.Invoke (null, args); Console.WriteLine (successfulParse + " " + args[1]); // True 23 |
Динамический вызов методов можно осуществлять с помощью делегатов, что значительно ускорит выполнение:
1 2 3 4 5 6 7 8 9 | delegate string StringToString (string s); static void Main() { MethodInfo trimMethod = typeof (string).GetMethod ("Trim", new Type[0]); var trim = (StringToString) Delegate.CreateDelegate (typeof (StringToString), trimMethod); for (int i = 0; i < 1000000; i++) trim ("test"); } |
BindingFlags и доступ к непубличным членам
Всем методам Get*
, указанным выше, можно дополнительно передатьenum BindingFlags
, который служит своеобразным фильтром для извлекаемых метаданных и позволяет изменить стандартные критерии отбора. В частности с его помощью можно извлечь непубличные члены:
1 2 3 4 5 6 7 8 9 10 11 12 | class Walnut { private bool cracked; public void Crack() { cracked = true; } public override string ToString() { return cracked.ToString(); } } Type t = typeof (Walnut); Walnut w = new Walnut(); w.Crack(); FieldInfo f = t.GetField ("cracked", BindingFlags.NonPublic | BindingFlags.Instance); f.SetValue (w, false); Console.WriteLine (w); // False |
Члены 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
, который затем можно вызвать:
1 2 3 4 | MethodInfo echo = typeof (Program).GetMethod ("Echo"); MethodInfo intEcho = echo.MakeGenericMethod (typeof (int)); Console.WriteLine (intEcho.IsGenericMethodDefinition); // False Console.WriteLine (intEcho.Invoke (null, new object[] { 3 } )); // 3 |
MethodBody
Для получения информации о содержимом метода необходимо вызвать GetMethodBody
на объекте метод MethodBase
. Метод вернет объект MethodBody
, который имеет свойства для инспектирования локальных переменных метода, конструкций обработки исключений, размера стэке и кода IL.
Динамическая генерация кода
С помощью класса DynamicMethod
из пространства имен System.Reflection.Emit
можно генерировать методы налету:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class Test { static void Main() { var dynMeth = new DynamicMethod ("Foo", // Название метода null, // Возвращаемый тип null, // Типы аргументов typeof (Test)); // Родительский тип ILGenerator gen = dynMeth.GetILGenerator(); gen.EmitWriteLine ("Hello world"); gen.Emit (OpCodes.Ret); dynMeth.Invoke (null, null); // Hello world } } |
Конструктор класса принимает четыре обязательных аргумента:
string
— название методаType
— возвращаемый методом тип (null
в случаеvoid
)Type[]
— массив типов аргументов (null
если метод не принимает аргументов)Type
— тип, к которому будет относиться метод
Класс OpCodes
имеет статические поля (доступные только для чтения) для каждого кода операции IL. Такое поле передается методу Emit
экземпляра ILGenerator
для выполнения соответствующей операции. Большая часть функционала реализуется именно таким способом, хотя класс ILGenerator
имеет специализированные методы для генерации меток, локальных переменных, обработки исключений.
Динамически генерируемый метод всегда заканчивается кодом операции Opcodes.Ret
, что означает возврат из метода.
Метод EmitWriteLine
экземпляра ILGenerator
— это сокращение для вызова ряда кодов операций:
1 2 3 4 | MethodInfo writeLineStr = typeof (Console).GetMethod ("WriteLine", new Type[] { typeof (string) }); gen.Emit (OpCodes.Ldstr, "Hello world"); // Загрузить строку gen.Emit (OpCodes.Call, writeLineStr); // Вызвать метод |
Конструктору DynamicMethod
четвертым аргументом можно передать тот или иной тип, в результате чего создаваемый динамический метод получит доступ к непубличным методам данного типа:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class Test { static void Main() { var dynMeth = new DynamicMethod ("Foo", null, null, typeof (Test)); ILGenerator gen = dynMeth.GetILGenerator(); MethodInfo privateMethod = typeof(Test).GetMethod ("HelloWorld", BindingFlags.Static | BindingFlags.NonPublic); gen.Emit (OpCodes.Call, privateMethod); // Вызов частного метода HelloWorld gen.Emit (OpCodes.Ret); dynMeth.Invoke (null, null); // Hello world } static void HelloWorld () // частный метод, но мы можем его вызвать { Console.WriteLine ("Hello world"); } } |
Стэк оценки (Evaluation Stack)
Центральной концепцией в IL является стэк оценки: с его помощью можно передавать значения в методы, а также получать возвращаемые методом значения. Для того чтобы вызвать метод с аргументами, аргументы сначала необходимо поместить в стэк оценки. После этого вызывается метод, который по мере необходимости извлекает аргументы из стэка и помещает в него свой результат.
Код операции OpCodes.Ldstr
позволяет поместить в стэк строку, коды OpCodes.Ldc*
— числовые литералы различных типов и размеров:
1 2 3 4 5 6 7 8 | var dynMeth = new DynamicMethod ("Foo", null, null, typeof(void)); ILGenerator gen = dynMeth.GetILGenerator(); MethodInfo writeLineInt = typeof (Console).GetMethod ("WriteLine", new Type[] { typeof (int) }); gen.Emit (OpCodes.Ldc_I4, 123); // Поместить число в стэк gen.Emit (OpCodes.Call, writeLineInt); gen.Emit (OpCodes.Ret); dynMeth.Invoke (null, null); // 123 |
Для сложения двух чисел их необходимо поместить в стэк, затем вызвать код Add
, который извлечет числа, сложит их и поместит результат обратно в стэк:
1 2 3 4 | gen.Emit (OpCodes.Ldc_I4, 2); gen.Emit (OpCodes.Ldc_I4, 2); gen.Emit (OpCodes.Add); gen.Emit (OpCodes.Call, writeLineInt); |
Аналогично и с другими вычислениями:
1 2 3 4 5 6 7 8 9 10 11 12 13 | gen.Emit (OpCodes.Ldc_I4, 10); gen.Emit (OpCodes.Ldc_I4, 2); gen.Emit (OpCodes.Div); gen.Emit (OpCodes.Ldc_I4, 1); gen.Emit (OpCodes.Add); gen.Emit (OpCodes.Call, writeLineInt); // или так: gen.Emit (OpCodes.Ldc_I4, 1); gen.Emit (OpCodes.Ldc_I4, 10); gen.Emit (OpCodes.Ldc_I4, 2); gen.Emit (OpCodes.Div); gen.Emit (OpCodes.Add); gen.Emit (OpCodes.Call, writeLineInt); |
Передача аргументов и возврат значения из динамического метода
Чтобы использовать аргументы, переданные динамическому методу, их также нужно загрузить в стэк оценки. Сделать это можно с помощью кодов Ldarg
и Ldarg_XXX
(XXX
— номер аргумента). Возврат значений из динамического метода также осуществляется через стэк оценки. Для этого необходимо перед выходом из метода оставить в стэке ровно одно значение.
Чтобы все это работало, создавая динамический метод конструктору DynamicMethod
нужно передать возвращаемый тип и типы аргументов:
1 2 3 4 5 6 7 8 9 10 | DynamicMethod dynMeth = new DynamicMethod ("Foo", typeof (int), // Возвращаемый тип - int new[] { typeof (int), typeof (int) }, // Типы параметров - int, int typeof (void)); ILGenerator gen = dynMeth.GetILGenerator(); gen.Emit (OpCodes.Ldarg_0); // Поместить первый аргумент в стэк gen.Emit (OpCodes.Ldarg_1); // Поместить второй аргумент в стэк gen.Emit (OpCodes.Add); // Сложить аргументы и поместить результат в стэк gen.Emit (OpCodes.Ret); // Возврат единственного значения в стэке int result = (int) dynMeth.Invoke (null, new object[] { 3, 4 } ); // 7 |
По завершении метода стэк должен иметь в точности 0 (если метод не возвращает значения) или 1 значение. Если нарушить это правило метод не будет выполнен. Удалить элемент из стэка без обработки можно кодом OpCodes.Pop
.
Генерация локальных переменных
Объявить локальную переменную можно с помощью вызова метода DeclareLocal
на экземпляре ILGenerator
. Метод вернет объект LocalBuilder
, который можно использовать в сочетании с кодами Ldloc
— помещает значение переменной в стэк для дальнейшего использования, и Stloc
— извлекает значение из стэка и сохраняет его в локальной переменной:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | var dynMeth = new DynamicMethod ("Test", null, null, typeof (void)); ILGenerator gen = dynMeth.GetILGenerator(); LocalBuilder localX = gen.DeclareLocal (typeof (int)); // Объявление x LocalBuilder localY = gen.DeclareLocal (typeof (int)); // Объявление y gen.Emit (OpCodes.Ldc_I4, 6); // Поместить в стэк литерал 6 gen.Emit (OpCodes.Stloc, localX); // Сохранить 6 в localX gen.Emit (OpCodes.Ldc_I4, 7); // Поместить в стэк литерал 7 gen.Emit (OpCodes.Stloc, localY); // Сохранить его в localY gen.Emit (OpCodes.Ldloc, localX); // Поместить localX в стэк gen.Emit (OpCodes.Ldloc, localY); // Поместить localY в стэк gen.Emit (OpCodes.Mul); // Перемножить значения gen.Emit (OpCodes.Stloc, localX); // Сохранить результат в localX gen.EmitWriteLine (localX); // Вывести значение localX gen.Emit (OpCodes.Ret); dynMeth.Invoke (null, null); // 42 |
Ветвление
Циклы вроде while
, do
и for
в IL не предусмотрены. Вместо этого ветвление выполняется с помощью меток и эквивалентов оператора goto
. Имеются коды операций ветвления:
Br
— безусловное ветвлениеBrtrue
— ветвление, если значение в стэке оценки равноtrue
Blt
— ветвление, если первое значение в стэке меньше второго
Для установки цели ветвления сначала нужно вызвать DefineLabel
(вернет объект Label
), а затем вызвать MarkLabel
в месте, где должна быть установлена метка:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | ILGenerator gen = ... Label startLoop = gen.DefineLabel(); // Объявить метку Label endLoop = gen.DefineLabel(); LocalBuilder x = gen.DeclareLocal (typeof (int)); // int x gen.Emit (OpCodes.Ldc_I4, 5); gen.Emit (OpCodes.Stloc, x); // x = 5 gen.MarkLabel (startLoop); gen.Emit (OpCodes.Ldc_I4, 10); // Загрузить в стэк 10 gen.Emit (OpCodes.Ldloc, x); // Загрузить в стэк x gen.Emit (OpCodes.Blt, endLoop); // if (x > 10) goto endLoop gen.EmitWriteLine (x); // Console.WriteLine (x) gen.Emit (OpCodes.Ldloc, x); // Загрузить в стэк x gen.Emit (OpCodes.Ldc_I4, 1); // Загрузить в стэк 1 gen.Emit (OpCodes.Add); // Сложить их gen.Emit (OpCodes.Stloc, x); // Сохранить результат в x gen.Emit (OpCodes.Br, startLoop); // Вернуться в начало цикла gen.MarkLabel (endLoop); gen.Emit (OpCodes.Ret); |
Создание объектов и вызов их методов
Создать объект можно с помощью кода операции Newobj
, которая вызовет конструктор и загрузит созданный экземпляр в стэк оценки:
1 2 3 4 | var dynMeth = new DynamicMethod ("Test", null, null, typeof (void)); ILGenerator gen = dynMeth.GetILGenerator(); ConstructorInfo ci = typeof (StringBuilder).GetConstructor (new Type[0]); gen.Emit (OpCodes.Newobj, ci); |
После этого можно вызывать экземплярные методы объекта с помощью кодов Call
или Callvirt
:
1 2 3 4 | gen.Emit (OpCodes.Callvirt, typeof (StringBuilder).GetProperty ("MaxCapacity").GetGetMethod()); gen.Emit (OpCodes.Call, typeof (Console).GetMethod ("WriteLine", new[] { typeof (int) } )); gen.Emit (OpCodes.Ret); dynMeth.Invoke (null, null); // 2147483647 |
Call
используется для обращения к статическим методам (значимых и ссылочных типов) и экземплярным методам значимых типов, а Callvirt
— для обращения к экземплярным методам ссылочных типов.
Имитирование сборок и типов
Класс DynamicMethod
может генерировать только методы, а если необходимо сгенерировать, например, тип, схема усложняется. Тип не может существовать сам по себе: он должен находиться в модуле внутри сборки. Добавить тип в существующую сборку нельзя — сборка является неизменяемой после создания. Поэтому потребуется динамически создать новую сборку и модуль. Однако сборка необязательно должна присуствовать на диске, она может располагаться в памяти. Создать сборку и модуль можно с помощью классов AssemblyBuilder
и ModuleBuilder
:
1 2 3 4 5 | AppDomain appDomain = AppDomain.CurrentDomain; AssemblyName aname = new AssemblyName ("MyDynamicAssembly"); AssemblyBuilder assemBuilder = appDomain.DefineDynamicAssembly (aname, AssemblyBuilderAccess.Run); ModuleBuilder modBuilder = assemBuilder.DefineDynamicModule ("DynModule"); |
Динамические сборки не подвержены сборке мусора и остаются в памяти до завершения домена приложения.
Имея модуль можно воспользоваться классом TypeBuilder
для создания типа:
1 | TypeBuilder tb = modBuilder.DefineType ("Widget", TypeAttributes.Public); |
Перечисление TypeAttributes
задает различные настройки для типа: оно имеет члены, соответствующие модификаторам доступа, Abstract
или Sealed
— для объявления класса абстрактным или закрытым, Interface
— для определения интерфейса, Serializable
— эквивалентен применению атрибута[Serializable]
, Explicit
— эквивалентен применению атрибута [StructLayout(LayoutKind.Explicit)]
.
Методу DefineType
также можно передать необязательный атрибут, указывающий базовый класс:
- для определения структуры нужно передать
System.ValueType
- для определения делегата —
System.MulticastDelegate
- для реализации интерфейсов — массив типов интерфейсов
- для определиния интерфейса —
TypeAttributes.Interface | TypeAttributes.Abstract
После создания типа можно создавать члены внутри него:
1 2 3 4 5 6 | MethodBuilder methBuilder = tb.DefineMethod ("SayHello", MethodAttributes.Public, null, null); ILGenerator gen = methBuilder.GetILGenerator(); gen.EmitWriteLine ("Hello world"); gen.Emit (OpCodes.Ret); |
После определения членов можно завершить объявление типа:
1 | Type t = tb.CreateType(); |
А после создания типа можно создавать его экземпляры и вызывать его методы:
1 2 | object o = Activator.CreateInstance (t); t.GetMethod ("SayHello").Invoke (o, null); // Hello world |
Созданную сборку можно сохранить в файл на диск с помощью метода Save
класса AssemblyBuilder
, которому нужно передать имя файла. Но предварительно нужно выполнить два действия:
- при создании объекта
AssemblyBuilder
передать в конструктор флагAssemblyBuilderAccess.Save
илиAssemblyBuilderAccess.RunAndSave
- при создании объекта
ModuleBuilder
указать имя файла
Можно также дополнительно установить свойства объекта AssemblyName
, такие как Version
или KeyPair
:
1 2 3 4 5 6 7 | AppDomain domain = AppDomain.CurrentDomain; AssemblyName aname = new AssemblyName ("MyEmissions"); aname.Version = new Version (2, 13, 0, 1); AssemblyBuilder assemBuilder = domain.DefineDynamicAssembly (aname, AssemblyBuilderAccess.RunAndSave); ModuleBuilder modBuilder = assemBuilder.DefineDynamicModule ("MainModule", "MyEmissions.dll"); // Создание типов assemBuilder.Save ("MyEmissions.dll"); |
По умолчанию файл записывается в базовый каталог приложения. Чтобы сохранить сборку в другом месте при создании объекта AssemblyBuilder
нужно указать альтернативный каталог:
1 2 | AssemblyBuilder assemBuilder = domain.DefineDynamicAssembly ( aname, AssemblyBuilderAccess.RunAndSave, @"d:\assemblies" ); |
После сохранения в файл динамическая сборка становится обычной сборкой.
Имитирование членов
Имитирование методов
Методы можно создавать не только с помощью класса DynamicMethod
, но и с помощью класса DefineMethod
. При вызове DefineMethod можно указывать возвращаемый тип и типы принимаемых параметров:
1 2 3 4 5 6 7 8 9 10 11 12 13 | MethodBuilder mb = tb.DefineMethod ("SquareRoot", MethodAttributes.Static | MethodAttributes.Public, CallingConventions.Standard, typeof (double), // Возвращаемый тип new[] { typeof (double) } ); // Типы параметров mb.DefineParameter (1, ParameterAttributes.None, "value"); // Объявление параметра ILGenerator gen = mb.GetILGenerator(); gen.Emit (OpCodes.Ldarg_0); // Загрузить первый аргумент gen.Emit (OpCodes.Call, typeof(Math).GetMethod ("Sqrt")); gen.Emit (OpCodes.Ret); Type realType = tb.CreateType(); double x = (double) tb.GetMethod ("SquareRoot").Invoke (null, new object[] { 10.0 }); Console.WriteLine (x); // 3.16227766016838 |
Вызов DefineParameter
является необязательным. С его помощью можно присвоить параметру имя, что обычно имеет смысл при сохранении сборки на диск делая метод более дружественным для потребителя. Число 1 ссылается на первый параметр (0 соответствует возвращаемому значению). Метод DefineParameter
возвращает объект ParameterBuilder
, на котором можно вызвать SetCustomAttribute
для присоединения атрибутов к параметру.
Для параметров передаваемых по ссылке необходимо вызвать MakeByRefType
на типе параметра:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | MethodBuilder mb = tb.DefineMethod ("SquareRoot", MethodAttributes.Static | MethodAttributes.Public, CallingConventions.Standard, null, new Type[] { typeof (double).MakeByRefType() } ); mb.DefineParameter (1, ParameterAttributes.None, "value"); ILGenerator gen = mb.GetILGenerator(); gen.Emit (OpCodes.Ldarg_0); gen.Emit (OpCodes.Ldarg_0); gen.Emit (OpCodes.Ldind_R8); // Загрузить косвенно 8-битное число gen.Emit (OpCodes.Call, typeof (Math).GetMethod ("Sqrt")); gen.Emit (OpCodes.Stind_R8); // Сохранить косвенно gen.Emit (OpCodes.Ret); Type realType = tb.CreateType(); object[] args = { 10.0 }; tb.GetMethod ("SquareRoot").Invoke (null, args); Console.WriteLine (args[0]); // 3.16227766016838 |
Параметры out
определяются аналогично, за исключением того, что DefineParameter
вызывается следующим образом:
1 | mb.DefineParameter (1, ParameterAttributes.Out, "value"); |
Для генерации экземплярного метода, при вызове DefineMethod
необходимо указать MethodAttributes.Instance
:
1 2 3 | MethodBuilder mb = tb.DefineMethod ("SquareRoot", MethodAttributes.Instance | MethodAttributes.Public ... |
В экземплярных методах нулевым аргументом по умолчанию является this
, остальные аргументы нумеруются начиная c 1, поэтому Ldarg_0
загрузит в стэк оценки this
, а Ldarg_1
— первый реальный аргумент метода.
При переопределении методов базового класса в производном полезно указыватьMethodAttributes.HideBySig
. Он указывает на необходимость скрывать переопределяемый метод базового класса только если он полностью совпадает по сигнатуре. Без этого сокрытие основывается на имени.
Имитирование поле и свойств
Для создания поля необходимо вызвать DefineField
на объекте TypeBuilder
, передав ему имя поля, тип и видимость:
1 2 3 | FieldBuilder field = tb.DefineField ("length", typeof (int), FieldAttributes.Private); |
Создание свойств и индексаторов несколько сложнее. Сначала необходимо вызвать DefineProperty
на объекте TypeBuilder
, передав ему имя и тип свойства:
1 2 3 4 5 6 | PropertyBuilder prop = tb.DefineProperty ( "Text", // Имя свойства PropertyAttributes.None, typeof (string), // Тип свойства new Type[0] // Типы индексатора ); |
Последний аргумент используется при создании индексатора и представляет собой массив типов для индексатора. Видимость свойства не указывается: она задается отдельно для каждого метода доступа.
После этого необходимо создать методы доступа к свойству get
и set
. По соглашению они именуются также как свойство, но с префиксом get_
и set_
. После создания методов их нужно присоединить к свойству с помощью вызова методов SetGetMethod
и SetSetMethod
на объекте PropertyBuilder
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | // Частное поле FieldBuilder field = tb.DefineField ("_text", typeof (string), FieldAttributes.Private); // Свойство для доступа к полю PropertyBuilder prop = tb.DefineProperty ( "Text", // Имя свойства PropertyAttributes.None, typeof (string), // Тип свойства new Type[0]); // Типы индексатора // Get метод MethodBuilder getter = tb.DefineMethod ( "get_Text", // Имя метода MethodAttributes.Public | MethodAttributes.SpecialName, typeof (string), // Возвращаемый тип new Type[0]); // Параметры типа ILGenerator getGen = getter.GetILGenerator(); getGen.Emit (OpCodes.Ldarg_0); // Загрузить this в стэк getGen.Emit (OpCodes.Ldfld, field); // Загрузить в стэк значение поля getGen.Emit (OpCodes.Ret); // Выход из метода // Set метод MethodBuilder setter = tb.DefineMethod ( "set_Text", MethodAttributes.Assembly | MethodAttributes.SpecialName, null, new Type[] { typeof (string) } ); ILGenerator setGen = setter.GetILGenerator(); setGen.Emit (OpCodes.Ldarg_0); // Загрузить this в стэк setGen.Emit (OpCodes.Ldarg_1); // Загрузить в стэк второй аргумент - value setGen.Emit (OpCodes.Stfld, field); // Сохранить value в поле setGen.Emit (OpCodes.Ret); // Возврат из метода prop.SetGetMethod (getter); // Присоединить get метод к свойству prop.SetSetMethod (setter); // Присоединить set метод к свойству |
События можно создавать аналогичным способом, вызывая метод DefineEvent
на объекте TypeBuilder
, а затем написав методы доступа и присоединив их к объекту EventBuilder
с помощью вызова методов SetAddOnMethod
и SetRemoveOnMethod
.
Имитирование конструкторов
Определить конструктор можно с помощью вызова метода DefineConstructor
на объекте TypeBuilder
. Делать это необязательно — если не определен ни один конструктор будет использован стандартный конструктор без параметров. Для подтипа стандартный конструктор вызовет конструктор базового класса. При определении конструктора (или нескольких) стандартный конструктор устраняется.
При имитации типов конструктор является единственным местом для инициализации полей:
1 2 3 4 5 6 7 8 9 10 11 | FieldBuilder field = tb.DefineField ("_capacity", typeof (int), FieldAttributes.Private); ConstructorBuilder c = tb.DefineConstructor ( MethodAttributes.Public, CallingConventions.Standard, new Type[0]); // Типы параметров конструктора ILGenerator gen = c.GetILGenerator(); gen.Emit (OpCodes.Ldarg_0); // Загрузить this в стэк gen.Emit (OpCodes.Ldc_I4, 4000); // Загрузить 4000 в стэк gen.Emit (OpCodes.Stfld, field); // Сохранить значение в поле gen.Emit (OpCodes.Ret); |
Конструктор базового класса по умолчанию не вызывается — он должен быть вызван явно в случае необходимости:
1 2 3 | gen.Emit (OpCodes.Ldarg_0); ConstructorInfo baseConstr = typeof (A).GetConstructor (new Type[0]); gen.Emit (OpCodes.Call, baseConstr); |
Присоединение атрибутов
Присоединить атрибут к динамически создаваемой конструкции можно с помощью метода SetCustomAttribute
соответствующего объекта-билдера с передачей ему экземпляра CustomAttributeBuilder
.
Например, чтобы присоединить к свойству или полю такой атрибут:
1 | [XmlElement ("FirstName", Namespace="http://test/", Order=3)] |
необходимо:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | Type attType = typeof (XmlElementAttribute); ConstructorInfo attConstructor = attType.GetConstructor ( new Type[] { typeof (string) } ); // Создаем атрибут var att = new CustomAttributeBuilder ( attConstructor, // Конструктор new object[] { "FirstName" }, // Аргументы конструктора new PropertyInfo[] { attType.GetProperty ("Namespace"), // Свойства attType.GetProperty ("Order") }, new object[] { "http://test/", 3 } // Значения свойств ); myFieldBuilder.SetCustomAttribute (att); // или propBuilder.SetCustomAttribute (att); // или typeBuilder.SetCustomAttribute (att); и т.д. |
Имитирование обобщений
Обобщенные методы
Для генерации обобщенного метода необходимо:
- вызвать
DefineGenericParameters
на объектеMethodBuilder
для получения массива объектовGenericTypeParameterBuilder
- вызвать
SetSignature
на объектеMethodBuilder
, передав ему полученный в первом пункте массив параметров типа - назначить параметрам имена (необязательно)
1 2 3 4 5 6 7 8 9 10 11 12 | TypeBuilder tb = modBuilder.DefineType ("Widget", TypeAttributes.Public); MethodBuilder mb = tb.DefineMethod ("Echo", MethodAttributes.Public | MethodAttributes.Static); GenericTypeParameterBuilder[] genericParams = mb.DefineGenericParameters ("T"); mb.SetSignature (genericParams[0], // Возвращаемый тип null, null, genericParams, // Параметры типа null, null); mb.DefineParameter (1, ParameterAttributes.None, "value"); // Необязательно ILGenerator gen = mb.GetILGenerator(); gen.Emit (OpCodes.Ldarg_0); gen.Emit (OpCodes.Ret); |
DefineGenericParameters
может принимать любое количество строковых аргументов, которые соответствуют именам желаемых обобщенных типов.Класс GenericTypeParameterBuilder
позволяет указывать ограничение базового типа и ограничения интерфейсов:
1 2 3 4 5 6 7 8 9 10 11 12 | // Ограничение базового типа: genericParams[0].SetBaseTypeConstraint (typeof (Foo)); // Ограничения интерфейсов: genericParams[0].SetInterfaceConstraints (typeof (IComparable)); // Чтобы сымитировать инструкцию: public static T Echo<T> (T value) where T : IComparable<T> // необходимо: genericParams[0].SetInterfaceConstraints ( typeof (IComparable<>).MakeGenericType (genericParams[0]) ); |
Для других ограничений необходимо вызывать метод SetGenericParameterAttributes
и передавать ему enum GenericParameterAttributes
, который включает следующие значения:
DefaultConstructorConstraint
NotNullableValueTypeConstraint
ReferenceTypeConstraint
Covariant
(эквивалент модификатораout
)Contravariant
(эквивалент модификатораin
)
Обобщенные типы
Обобщенные типы определяются аналогично, только метод DefineGenericParameters
вызывается на объекте TypeBuilder
:
1 2 3 4 | TypeBuilder tb = modBuilder.DefineType ("Widget", TypeAttributes.Public); GenericTypeParameterBuilder[] genericParams = tb.DefineGenericParameters ("T"); tb.DefineField ("Value", genericParams[0], FieldAttributes.Public); |