Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Объектно-ориентированное программирование.-6

.pdf
Скачиваний:
8
Добавлен:
05.02.2023
Размер:
4.5 Mб
Скачать

static void GetPropertiesInfo(Type t)

{

PropertyInfo[] props = t.GetProperties();

Console.WriteLine(" Свойства: {0}", props.Length > 0 ? "" :

"нет");

foreach (var prop in props)

{

ParameterInfo[] pinfo = prop.GetIndexParameters();

Console.Write(" {0} ", GetTypeStr(prop.PropertyType)); if (pinfo.Length > 0) Console.Write("this[{0}] {{",

GetParamsInfo(pinfo));

else Console.Write("{0} {{", prop.Name); if (prop.CanRead) Console.Write(" {0}",

GetMethodInfo(prop.GetGetMethod(), "get"));

if (prop.CanWrite) Console.Write(" {0}", GetMethodInfo(prop.GetSetMethod(), "set"));

Console.WriteLine(" }");

}

}

static void GetTypesInfo(Type t)

{

Type[] types = t.GetNestedTypes();

Console.WriteLine(" Вложенные типы: {0}", types.Length > 0 ? "" :

"нет");

foreach (var type in types)

{

Console.WriteLine(" {0}", type);

}

}

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

GetMembersInfo(typeof(DateTime));

GetMembersInfo(typeof(IEnumerator));

5.3.2. Работа со сборками и модулями

Сборка (assembly) – это физический файл, состоящий из нескольких РЕ-файлов .NET. Главное преимущество сборок заключается в том, что они позволяют семантически группировать функциональность, что облегчает развертывание приложения и управление его версиями. Представлением сборки в .NET является класс System.Reflection.Assembly. Класс Assembly

позволяет выполнять множество действий, в том числе:

перечисление модулей сборки;

просмотр типов в сборке;

391

определение идентификационной информации, такой, как имя и местоположение физического файла сборки;

изучение информации о версиях и защите;

получение точек входа сборки.

Пример: Samples\5.3\5_3_2_assembly.

5.3.2.1. Просмотр типов сборки

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

using System;

using System.Diagnostics; using System.Reflection; using System.IO;

namespace Test

{

public enum Enum1 { } internal enum Enum2 { }

class AssemblyInfo

{

public static void Get(string name)

{

Assembly a = Assembly.LoadFile(name); Type[] types = a.GetTypes();

Console.WriteLine("Сборка: {0}", a.GetName()); Console.WriteLine("Типы:");

foreach (var type in types) Console.WriteLine(" {0}",

type);

Console.WriteLine();

}

}

}

namespace AssemblySample

{

class Program

{

static int Main()

{

string dir = Directory.GetCurrentDirectory(); string pname =

Process.GetCurrentProcess().ProcessName;

Test.AssemblyInfo.Get(dir + "\\" + pname + ".exe"); pname = dir +

"\\..\\..\\..\\5_3_1_reflection\\bin\\debug\\5_3_1_reflection.exe";

if (File.Exists(pname)) Test.AssemblyInfo.Get(pname);

return 0;

392

}

}

}

Для работы этого примера требуется наличие исполняемого файла предыдущего примера (5_3_1_reflection). В результате на консоли мы увидим информацию о типах текущей сборки и сборки 5_3_1_reflection:

Сборка: 5_3_2_assembly, Version=1.0.3931.25329, Culture=neutral,

PublicKeyToken=null

Типы: Test.Enum1 Test.Enum2

Test.AssemblyInfo

AssemblySample.Program

Сборка: 5_3_1_reflection, Version=1.0.3931.21376, Culture=neutral,

PublicKeyToken=null

Типы:

ReflectionSample.Program

Если программу запустить в режиме тестирования, то увидим следующую информацию о сборке:

Сборка: vshost, Version=9.0.0.0, Culture=neutral,

PublicKeyToken=b03f5f7f11d50a3a

Типы:

Microsoft.VisualStudio.HostingProcess.EntryPoint

Так происходит потому, что сначала запускается отладчик, а уже затем он вызывает наше приложение.

5.3.2.2. Вывод списка модулей сборки

Все созданные нами ранее сборки состояли из одного модуля. Однако, к сборке можно подключать дополнительные модули. Это файлы, имеющие расширение .NETMODULE. К сожалению, создать такой файл из среды разработки нельзя (по крайней мере, настройки проекта в среде Visual Studio 2008 позволяют задать только три типа выходных данных – приложение Windows, консольное приложение и библиотека классов). Поэтому необходимо использовать командную строку компилятора:

csc.exe /target:module ...

Создадим файл следующего содержания

namespace MyModule

{

class MyClass { } struct MyStruct { }

delegate void MyDelegate();

393

}

и сохраним его под именем «ExtModule.cs». Далее откомпилируем его:

csc.exe /target:module ExtModule.cs

Может потребоваться указание полного пути к компилятору C#. В каталоге примера находится пакетный файл «csext.bat», содержащий полную командную строку для .NET Framework версии 3.5. При необходимости номер версии можно исправить (см. п. 2.3.3). В итоге мы получили файл модуля «ExtModule.netmodule». Теперь его надо добавить в сборку, указав параметр компилятора

csc.exe /addmodule:<имя модуля> ...

Например:

csc.exe /out:5_3_2_assembly.exe /addmodule:ExtModule.netmodule Program.cs Properties\AssemblyInfo.cs

В каталоге примера находится пакетный файл «csass.bat», содержащий полную командную строку для .NET Framework версии 3.5. Чтобы не набирать опции компилятора вручную, можно их скопировать из окна вывода данных о построении проекта (см. п. 4.1.3.2). Затем некоторые опции можно добавить, убрать или изменить.

После запуска файла «5_3_2_assembly.exe» из консоли видим резуль-

тат:

Состав текущей сборки:

Модуль: 5_3_2_assembly.exe

Типы:

Test.Enum1

Test.Enum2

Test.AssemblyInfo

AssemblySample.Program

Модуль: ExtModule.netmodule

Типы:

MyModule.MyClass

MyModule.MyStruct

MyModule.MyDelegate

5.3.3. Позднее связывание и отражение

Позднее связывание (late binding) в программировании – это ситуация, когда компилятор ничего не знает о некоторых типах и их членах во время компоновки, но, тем не менее, специальные средства позволяют их использовать во время выполнения программы. В противовес этому ситуация, когда

394

информация о типе во время компиляции известна, называется ранним свя-

зыванием (early binding).

Примером позднего связывания может служить вызов динамических библиотек (DLL) в языке C++. Типы данных, описанные в них, использовать нельзя, но процедуры и функции – вполне. Еще один пример позднего связывания – использование COM-интерфейсов.

В среде .NET можно использовать динамические библиотеки, написанные на других языках. Это рассматривается как выполнение неуправляемого кода (см. § 5.5). Однако, если библиотека содержит управляемый код .NET, доступ к типам, описанным в такой библиотеке, можно получить посредством отражения (используя классы Assembly и System.Activator).

Сначала создадим проект «5_3_3_latebind». Поместим в файл «Program.cs» код следующего содержания:

using System;

using System.Reflection; using System.IO;

namespace LateBindingSample

{

class Program

{

static int Main()

{

string[] dlls = Directory.GetFiles(Directory.GetCurrentDirectory() + "\\..\\..\\", "*.dll");

string str = "Здравствуй, мир!"; int action;

ConsoleKeyInfo key; object[] args;

foreach (string dll in dlls)

{

Assembly a = Assembly.LoadFrom(dll); Type[] types = a.GetTypes();

// дальнейшие действия

}

Console.ReadKey(true); return 0;

}

}

}

Итак, данный код перебирает все библиотеки в папке проекта (т.к. текущая папка при исполнении приложения – это <папка проекта>\bin\Debug или <папка проекта>\bin\Release, то, поднимаясь на два уровня вверх, попадаем в папку проекта). Затем загружает сборку из каждой найденной библио-

395

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

foreach (Type type in types)

{

if (!type.IsAbstract)

{

MethodInfo mi1 = type.GetMethod("GetName"); MethodInfo mi2 = type.GetMethod("GetOptions"); MethodInfo mi3 = type.GetMethod("Convert"); PropertyInfo pi = type.GetProperty("Data");

if (mi1 != null && mi2 != null && mi3 != null && pi != null)

{

object obj = Activator.CreateInstance(type); string name = (string)mi1.Invoke(obj, null); string[] opts = (string[])mi2.Invoke(obj, null);

Console.WriteLine("Найдена DLL: {0}", name);

Console.WriteLine("Выберите действие:");

for (int i = 0; i < opts.Length; i++) Console.WriteLine("{0}) {1}", i + 1, opts[i]);

do

{

key = Console.ReadKey(true); action = (int)key.KeyChar - 48;

} while (action < 1 || action > opts.Length);

Console.WriteLine(action); args = new object[1] { str };

pi.GetSetMethod().Invoke(obj, args); args = new object[1] { action - 1 };

Console.WriteLine(mi3.Invoke(obj, args));

}

}

}

Происходит поиск типов, имеющих методы GetName (чтобы узнать название операции), GetOptions (список доступных действий), Convert (выполнение операции) и свойство Data (доступ к инкапсулированной строке).

Далее создадим динамическую библиотеку. Для этого создадим новый проект, который назовем CaseConverter и сохраним его в папке проекта 5_3_3_latebind. Далее в опциях проекта («Проект» → «Свойства…») укажем:

Тип выходных данных – «Библиотека классов» (на закладке «Приложение»);

Путь вывода – «..\» (на закладке «Построение»), чтобы файл .DLL по-

мещался не в папку 5_3_3_latebind\CaseConverter\obj\Debug или Release, как это установлено по умолчанию, а в папку 5_3_3_latebind (мы поднимаемся всего на один уровень вверх, т.к. для данного проекта текущей является пап-

396

ка CaseConverter).

В файл «Program.cs» данной библиотеки поместим следующий код:

using System; using System.Text;

namespace StringConverters

{

public class CaseConverter

{

StringBuilder SB = new StringBuilder("");

static string[] Options = { "Верхний регистр", "Нижний регистр", "Инвертировать регистр", "Не изменять регистр" };

public string GetName()

{

return "Преобразование регистра";

}

public string[] GetOptions()

{

return Options;

}

public string Convert(string option)

{

for (int i = 0; i < SB.Length; i++)

{

if (option == 0) SB[i] = char.ToUpper(SB[i]); else if (option == 1) SB[i] =

char.ToLower(SB[i]);

else if (option == 2) SB[i] = char.IsLower(SB[i]) ? char.ToUpper(SB[i]) : char.ToLower(SB[i]);

else if (option != 3) throw new

ArgumentException("Неизвестная опция"); } return SB.ToString();

}

public string Data

{

get { return SB.ToString(); }

set { SB = new StringBuilder(value); }

}

}

class DLLEntryPoint

{

static int Main()

{

return 0;

}

}

}

Данный класс умеет изменять регистр строки. Он содержит все необходимые методы и свойства, поэтому пройдет проверку в главной программе. Выполняем построение DLL (выполнить ее нельзя), затем запускаем

397

главный проект. Результаты работы:

Найдена DLL: Преобразование регистра Выберите действие:

1)Верхний регистр

2)Нижний регистр

3)Инвертировать регистр

4)Не изменять регистр

3

зДРАВСТВУЙ, МИР!

Пример: Samples\5.3\5_3_3_latebind.

5.3.4. Создание и исполнение кода в период выполнения

Мы уже знаем, как использовать отражение для работы с типами в период выполнения (раннее связывание), а также как осуществлять позднее связывание и динамическое исполнение кода. Теперь рассмотрим динамическое создание кода в период выполнения, т.е. создание кода «на лету». При динамическом создании типов и их членов в период выполнения используются классы пространства имен System.Reflection.Emit:

Класс AssemblyBuilder (потомок Assembly) для динамического создания сборок;

Класс ConstructorBuilder (потомок ConstructorInfo) для динамического создания конструкторов;

Класс MethodBuilder (потомок MethodInfo) для динамического создания методов;

Класс FieldBuilder (потомок FieldInfo) для динамического создания

полей;

и многие другие.

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

Напишем программу, динамически создающую класс со статическим методом, выводящим на консоль строку «Hello, World!»:

using System;

using System.Reflection; using System.Reflection.Emit;

namespace EmittingSample

{

class Program

{

398

static int Main()

{

Console.WriteLine("Создаем динамический класс

EmitClass...");

AppDomain domain = AppDomain.CurrentDomain; AssemblyName aname = new AssemblyName("EmitAssembly"); AssemblyBuilder abuild =

domain.DefineDynamicAssembly(aname, AssemblyBuilderAccess.Run); ModuleBuilder modbuild =

abuild.DefineDynamicModule("EmitModule");

TypeBuilder tbuild = modbuild.DefineType("EmitClass",

TypeAttributes.Public);

MethodBuilder mbuild = tbuild.DefineMethod("HelloWorld1", MethodAttributes.Public | MethodAttributes.Static);

ILGenerator msil = mbuild.GetILGenerator(); msil.EmitWriteLine("Hello, World!"); msil.Emit(OpCodes.Ret);

mbuild = tbuild.DefineMethod("HelloWorld2", MethodAttributes.Public);

msil = mbuild.GetILGenerator(); msil.EmitWriteLine("Hello, World!"); msil.Emit(OpCodes.Ret);

Console.WriteLine("Создаем экземпляр класса

EmitClass...");

Type etype = tbuild.CreateType();

object eclass = Activator.CreateInstance(etype);

Console.WriteLine("Получаем информацию о методах

HelloWorld...");

MethodInfo hello1 = etype.GetMethod("HelloWorld1"); MethodInfo hello2 = etype.GetMethod("HelloWorld2");

Console.WriteLine("Вызываем статический метод

HelloWorld...");

hello1.Invoke(null, null); Console.WriteLine("Вызываем экземплярный метод

HelloWorld...");

hello1.Invoke(eclass, null); return 0;

}

}

}

Вывод на консоль:

Создаем динамический класс EmitClass...

Создаем экземпляр класса EmitClass...

Получаем информацию о методах HelloWorld...

Вызываем статический метод HelloWorld...

Hello, World!

Вызываем экземплярный метод HelloWorld...

Hello, World!

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

399

т.д.). Назовем сборку «EmitAssembly». Затем в текущем домене создаем динамическую сборку – экземпляр класса AssemblyBuilder, используя сформированное ранее описание сборки и указывая, что сборка будет только запускаться на исполнение (Run). В перечислении AssemblyBuilderAccess есть и другие константы, позволяющие записать сформированный код MSIL в файл и т.п. Затем в динамической сборке создаем новый динамический модуль «EmitModule» (экземпляр класса ModuleBuilder), а в нем – новый динамический тип «EmitClass» (экземпляр класса TypeBuilder) c модификатором public, чтобы он был виден за пределами сборки EmitAssembly. Далее в этом класса создаем статический метод HelloWorld1 и метод экземпляра HelloWorld2 (экземпляры класса MethodBuilder), оба с модификатором public. Класс ILGenerator позволяет добавлять код в динамические методы, включающий операторы выражений и операторы языка C# (метод Emit). Для некоторых операций и выражений есть специальные методы, например, метод EmitWriteLine для добавления в динамический код вызова метода Console.WriteLine. В данном случае в оба метода помещается код для вывода на консоль строки «Hello, World!» и оператор завершения метода return (он требуется обязательно, даже если метод ничего не возвращает).

Далее используем конструктор типов для создания метакласса описанного типа. После чего уже знакомый нам класс Activator используется для создания экземпляра класса EmitClass (он нам потребуется для вызова метода HelloWorld2). Затем также уже знакомый метод GetMethod получает информацию о методах. Далее мы эти методы вызываем, указывая ссылку на экземпляр класса для метода экземпляра. Параметров у методов нет, поэтому второй аргумент метода Invoke – null.

Пример: Samples\5.3\5_3_4_emit.

400