В главе 2 я говорил, что компилятор генерирует переносимый в Win32 исполняемый модуль (portable executable, РЕ), состоящий главным образом из MSIL-кода и метаданных. Одна из очень мощных возможностей .NET позволяет вам писать код, чтобы обращаться к метаданным приложения посредством
отражения
(reflection). Если просто, то отражение — это способность получать информацию о типе в период выполнения. В этой главе будет описан API отражения и способы его использования при обработке модулей и типов, входящих в сборки для получения различных характеристик типа, определенных в период разработки. Вы также познакомитесь с некоторыми усложненными способами применения отражения, такими как динамический вызов методов и использование информации о типе (через позднее связывание) и даже создание и исполнение MSIL-кода в период выполнения!
API отражения .NET представляет собой иерархию классов (рис. 16-1), определенную в пространстве имен
System.Reflection.
Эти классы позволяют логически прослеживать информацию о сборке и о типе. Вы можете начинать с любого места в иерархии в зависимости от конкретных потребностей при разработке приложения.
В эти классы входит изрядная доля функциональности. Я не буду перечислять все методы и поля каждого класса, а дам обзор ключевых классов и затем покажу пример; иллюстрирующий функциональность, которую вам скорее всего потребуется включить в свои приложения.
Центральное место в отражении занимает
System. Type —
абстрактный класс, который представляет тип в CTS (Common Type System) и позволяет запрашивать имя типа, включающий его модуль и пространство имен, а также является ли этот тип размерным или ссылочным.
Класс
System. Type
также позволяет запрашивать практически все атрибуты типа, включая модификатор доступа, является ли тип вложенным, его СОМ-свойства и т. д. Взгляните на этот код, использующий несколько обычных и демонстрационных типов:
using System;
using System.Reflection;
interface Demolnterface
{
}
class DemoAttr : System.Attribute
{
>
enum DemoEnum
{
>
public class DemoBaseClass
{
}
public class DemoDerivedClass : DemoBaseClass
< }
class DemoStruct
{
}
class QueryTypesApp {
public static void QueryType(string typeName)
{
try
{
Type type = Type.GetType(typeName);
Console.Writel_ine("Type name: {0}", type.FullName);
Console. Writel_ine( "\tHasElementType = {0}",
type.HasElementType);
Console.Writel_ine("\tIsAbstract = {0}", type.IsAbstract); Console.WriteLine("\tIsAnsiClass = {0}",
type.IsAnsiClass);
Console.WriteLine("\tIsArray = {0}", type.IsArray); Console.WriteLine("\tIsAutoClass = {0}",
type.IsAutoClass); Console.WriteLine("\tIsAutoLayout = {0}",
type.IsAutoLayout);
Console.WriteLine("\tIsByRef = {0}", type.IsByRef); Console.WriteLine("\tIsClass = {0}", type.IsClass); Console.WriteLine("\tIsCOMObject = {0}",
type.IsCOMObject); Console.WriteLine("\tIsContextful = {0}",
type.IsContextful);
Console.WriteLine("\tIsEnum = {0}", type.IsEnum); Console.WriteLine("\tIsExplicitLayout = {0}",
type.IsExplicit Layout);
Console.WriteLine("\tlslmport = {0}", type.lslmport); Console.WriteLine("\tlslnterface = {0}",
type.lslnterface); Console.WriteLine("\tIsLayoutSequential = {0}",
type.IsLayoutSequential); Console.WriteLine("\tIsMarshalByRef = {0}",
type.IsMarshalByRef); Console.WriteLine("\tIsNestedAssembly = {0}",
type.IsNestedAssembly); Console.WriteLine("\tIsNestedFamANDAssem = {0}",
type.IsNestedFamANDAssem); Console.WriteLine("\tIsNestedFamily = {0}",
type.IsNestedFamily); Console.WriteLine("\tIsNestedFamORAssera = {0}",
type.IsNestedFamORAssem); Console.WriteLine("\tIsNestedPrivate = {0}",
type.IsNestedPrivate); Console.WriteLine("\tIsNestedPublic = {0}",
type.IsNestedPublic); Console.WriteLine("\tIsNotPublic = {0}",
type.IsNotPublic); Console.WriteLine("\tIsPointer = {0}",
type.IsPointer); Console.WriteLine("\tIsPrimitive = {Q}",
type.IsPrimitive); Console.WriteLine("\tIsPublic = {O}",
type.IsPublic); Console.WriteLine("\tIsSealed = {0}",
type.IsSealed); Console.WriteLine("\tIsSeriallzable = {0}",
type.IsSerializable); Console.WriteLine("\tIsServicedComponent = {0}",
type.IsServicedComponent); Console.WriteLine("\tIsSpecialName = {0}",
type.IsSpecialName); Console.WriteLine("\tIsUnicodeClass = {0}",
type.IsUnicodeClass); Console.WriteLine("\tIsValueType = {0}",
type.IsValueType); }
catch(System.NullReferenceException) {
Console.WriteLine
("{0} is not a valid type", typeName); } }
public static void Main(string[] args) <
Que ryType("System.Int32");
QueryType("System.Int64");
QueryType("System.Type");
QueryTypeC'DemoAttr"); QueryType("DemoEnum");
QueryType("DemoBaseClass"); Que ryType("DemoDe rivedClass"); QueryTypeC'DemoStruct"); } }
Отражение позволяет получить информацию о типе в период выполнения. API отражения обеспечивает выполнение таких действий, как циклическая обработка модулей и типов сборки, получение различных характеристик типа периода разработки. Более сложные задачи, решаемые с помощью отражения, включают динамический вызов методов и использование типов (через позднее связывание) и даже создание и исполнение MSIL-кода в период выполнения.
Вот как можно получить объект
Туре
для экземпляра типа
inf.
using System;
using System.Reflection;
class TypeObjectFromlnstanceApp
{
public static void Main(string[] args) {
int 1=6;
Type t = i.GetTypeO; Console.WriteLine(t.Name); } }
Кроме получения объекта
Туре
из переменной, можно создавать этот объект на основании имени типа. Другими словами, иметь экземпляр типа не обязательно. Вот как это сделать для типа
System.Int32:
using System;
using System.Reflection;
class TypeObjectFromNameApp {
public static void Main(string[] args) {
Type t = Type.GetType("System.Int32"); Console.WriteLine(t.Name); } }
При вызове метода
Type.GetType
нельзя использовать псевдонимы С#, так как этот метод используется всеми языками. Поэтому вы не можете указывать применяемый в С# псевдоним
int
вместо
System.Int32.
Несколько лет назад я работал в IBM Multimedia division над продуктом IBM/World Book Multimedia Encyclopedia. Нам нужно было создать приложение, позволяющее пользователю настраивать коммуникационные протоколы для работы с серверами World Book. Это решение должно было быть динамическим, чтобы пользователи могли непрерывно добавлять в систему и удалять из нее различные протоколы (TCP/IP, IGN, CompuServ и т. д.). Однако это приложение должно было «знать», какие протоколы присутствуют в системе, чтобы пользователь мог выбрать конкретный протокол для настройки и применения. Мы решили создать DLL со специальными расширениями и установить их в папку приложения. Когда у пользователя возникало желание увидеть список установленных протоколов, приложение вызывало \Ут32-функцию
LoadLibrary
чтобы загрузить DLL, а затем — функцию
GetProcAddress,
чтобы получить указатель на нужную функцию. Это замечательный пример позднего связывания в традиционном \Ут32-программировании, когда компилятор ничего не знает об этих вызовах во время компоновки. Как вы увидите из следующего примера, в .NET ту же задачу позволяет решить класс
Assembly,
отражение типов и новый класс —
Activator.
Чтобы заставить все шестеренки этого механизма крутиться, создадим абстрактный класс
CommProtocol.
Я определю этот класс в его собственной DLL. В результате его могут совместно использовать несколько DLL, которым потребуются производные от него классы (обратите внимание на параметры командной строки в комментарии к коду). // CommProtocol.cs
// Компоновка со следующими переключателями командной строки: // esc /t:library commprotocol.es public abstract class CommProtocol {
public static string DLLMask = "CommProtocob.dll"; public abstract void DisplayNameO; }
А сейчас я создам две отдельные DLL, каждая из которых реализует какой-то коммуникационный протокол и содержит класс, производный от абстрактного класса
CommProtocol.
Заметьте: обе должны ссылаться на CommProtocol.dll при компиляции. Вот DLL для протокола IGN:
// CommProtocolI6N.cs
// Компоновка со следующими переключателями командной строки: // esc /t:libгагу CommProtocolIGN.cs /г:CommProtocol.dll using System;
public class CommProtooolIGN : CommProtocol {
public override void DisplayNameO {
Console.WriteLine("This is the IBM Global Network"); > }
А вот DLL для TCP/IP: // CommProtocolTcpIp.es
// Компоновка со следующими переключателями командной строки: // esc /t:library CommProtocolTcpIp.es /r:CommProtocol.dll using System;
public class CommProtocolTcpIp : CommProtocol {
public override void DisplayNameO {
Console.WriteLine("This is the TCP/IP protocol"); } }
Посмотрим, насколько легко осуществляется динамическая загрузка, поиск типа и создание его экземпляра, а также вызов одного из его методов (кстати, на прилагаемом к книге диске есть командный файл BuildLateBmdmg.cmd, который также осуществляет компоновку всех этих файлов):
using System;
using System.Reflection;
using System.10;
class LateBindingApp {
public static void Main()
<
string[] fileNames = Directory.GetFiles
(Environment.CurrentDirectory,
CommProtocol.DLLMask); foreach(string fileName in fileNames) {
Console.WriteLine("Loading DLL '{0}'", fileName);
Assembly a = Assembly.LoadFrom(fileName);
Type[] types = a.GetTypes(); foreach(Type t in types) {
if (t.IsSubclassOf(typeof(CommProtocol)))
{
object о = Activator.Createlnstance(t);
Methodlnfo mi = t.GetMethod("DisplayName");
Console.Write("\t"); mi.Invoke(o, null); }
else {
Console.WriteLine("\tThis DLL does not have " + "CommProtocol-derived class defined"); } } > > }
Сначала с помощью класса
System.IO. Directory
мы находим все DLL в данной папке по маске
CommProtocol*. dll.
Метод
Directory.GetFiles
вернет массив объектов типа
string,
представляющий имена файлов, соответствующих критерию поиска. Затем я могу задействовать цикл/огеасй для циклической обработки массива, вызывая метод
Assembly. LoadFrom,
о котором вы узнали выше. После создания сборки для данной DLL я циклически опрашиваю все типы сборки, вызывая метод
Type.SubClassOf,
чтобы определить, есть ли у сборки тип, производный от
CommProtocol. Я
предполагаю, что если будет найден хоть один такой тип, то я работаю с нужной DLL. Найдя сборку, у которой есть тип, производный от
CommProtocol, я
создаю экземпляр объекта
Activator
и передаю его конструктору объект
type.
Как вы, вероятно, догадались, класс
Activator
используется для динамического создания, или активизации, типа.
Затем я использовал метод
Ту ре.Get Method,
чтобы создать объект
Methodlnfo,
указав имя метода
DisplayName.
Сделав это, я могу задействовать метод
Invoke
объекта
Methodlnfo,
передавая ему активизированный тип, и — пожалуйста! — метод DLL
DisplayName
вызван!
Для последовательного просмотра всех типов данной сборки вам нужно лишь создать экземпляр объекта
Assembly
и запросить массив
Types
для этой сборки, например:
using System;
using System.Diagnostics;
using System.Reflection;
class DemoAttr : System.Attribute
< >
enum DemoEnum
{
}
class DemoBaseClass
{
}
class DemoDerivedClass : DemoBaseClass {
}
class DemoStruct
{
}
class GetTypesApp {
protected static string GetAssemblyName(string[] args)
{
string assemblyName;
if (0 == args.Length) {
Process p = Process.GetCurrentProcessQ;
assemblyName = p.ProcessName + ".exe"; } else
assemblyName = args[0]; return assemblyName; }
public static void Main(string[] args) <
string assemblyName = GetAssemblyName(args);
Console.WriteLine("Loading info for " + assemblyName); Assembly a = Assembly.LoadFrom(assemblyName); Type[] types = a.GetTypes(); foreach(Type t in types) {
Console.WM.teLine("\nType information for: " +
t.FullName); Console.WriteLine("\tBase class = " +
t.BaseType.FullName); } } }
Подробнее мы рассмотрим сборки в главе 18. Пока нам достаточно знать, что сборка (assembly) — это физический файл, состоящий из нескольких РЕ-файлов .NET. Главное преимущество сборки в том, что она позволяет семантически группировать функциональность, что облегчает развертывание приложения и управление его версиями. Представлением сборки периода выполнения в .NET является класс
Assembly
(он ^ке — вершина иерархии объектов отражения).
Класс Assembly позволяет выполнить много действий, в том числе:
просмотреть типы сборки; перечислить модули сборки; определить идентификационную информацию, такую как имя и местоположение физического файла сборки; изучить информацию о версиях и защите; получить точки входа сборки.
Вы уже видели, как отражать типы в период выполнения, осуществлять позднее связывание с кодом и динамическое исполнение кода. Сделаем следующий шаг в логической последовательности — рассмотрим создание кода «на лету». При создании типов в период выполнения используется пространство имен
System.Reflection.Emit.
С помощью классов из этого пространства имен можно определять сборку в памяти, создавать для нее модули, определять для модулей новые типы (и их члены) и даже генерировать коды операций MSIL для реализации прикладной логики.
Несмотря на чрезвычайную простоту кода этого, примера, я отделил серверный код — DLL, содержащую класс, который создает метод
Hello-World, —
от клиентского кода, приложения, которое создает экземпляр класса, генерирующего код, и вызывает его метод
Hello World
(обратите внимание на переключатели компилятора в комментариях). Объяснение этого кода DLL приводится ниже:
using System;
using System.Reflection;
using System.Reflection.Emit;
namespace ILGenServer {
public class CodeGenerator {
public CodeGenerator() {
// Получить текущий currentDomain. currentDomain = AppDomain.CurrentDomain;
// Создать сборку в текущем currentDomain. assemblyName = new AssemblyNameQ; assemblyName.Name = "TempAssembly";
assemblyBuilder =
currentDomain.DefineDynamicAssembly
(assemblyName, AssemblyBuilderAccess.Run);
// Создать в этой сборке модуль moduleBuilder = assemblyBuilder.DefineOynamicModule ("TempModule");
// Создать тип в этой сборке typeBuilder = moduleBuilder.DefineType ("TempClass", TypeAttributes.Public); // Добавить к типу член (метод) methodBuilder = typeBuilder.DefineMethod ("HelloWorld", MethodAttributes.Public, null,null);
// Генерировать MSIL.
msil = methodBuilder.GetlLGeneratorO;
rasil.EmitWriteLine("Hello World
11
);
msil.Emit(OpCodes.Ret);
// Последний шаг: создание типа, t = typeBuilder.CreateTypeO;
>
AppDomain currentDomain; AssemblyName assemblyName; AssemblyBuilder assemblyBuilder; ModuleBuilder moduleBuilder; TypeBuilder typeBuilder; MethodBuilder roethodBuilder; ILGenerator msil; object o;
Type t; public Type T {
get {
return this.t; } > } }
Сначала мы создали экземпляр объекта
AppDomain
из текущей области (в главе 17 вы увидите, что прикладные области в функциональном плане похожи на процессы Win32). После этого мы создали экземпляр
объекта Assembly Name.
Класс
AssemblyName
подробно описан в главе 18, а вообще этот класс используется диспетчером кэша сборки для получения информации о ней. Получив текущую прикладную область и инициализированное имя сборки, вызываем метод
AppDomain.Defme-DynamicAssembly,
чтобы создать новую сборку. Заметьте, что два передаваемые нами аргумента являются именем сборки и описанием режима доступа к ней.
Assembly Builder Access.Run
указывает, что сборка может быть исполнена из памяти, но не может быть сохранена. Метод
AppDomain. Define DynamicAssembly
возвращает объект
Assembly Builder,
который мы затем приводим к объекту
Assembly.
На этом этапе у нас есть полнофункциональная сборка в памяти. Теперь нам нужно создать ее временный модуль и его тип.
Начнем с вызова метода
Assembly. DefineDynamicModule
для получения объекта
ModuleBuilder.
Получив этот объект, мы вызываем его метод
DefineType,
чтобы создать объект
ТуреВтШег,
передавая методу имя типа («
TempClass»)
и используемые для его определения атрибуты (
TypeAttri-butes.Public).
Теперь, имея объект
TypeBuilder,
можно создать член любого нужного нам типа. В данном случае мы создаем новый метод с помощью метода
TypeBuilder. DefineMethod.
В результате получаем совершенно новый тип
TempClass
с встроенным методом
HelloWorld.
Теперь все, что нам осталось сделать, — это решить, какой код поместить в этот метод. Для этого код создает экземпляр объекта
ILGenerator с
помощью метода
MethodBuilder.GetlLGenerator
и вызывает различные методы
IWenerator
для записи в метод MSIL-кода.
Здесь мы можем использовать стандартный код типа
Console. WriteLine
с помощью различных методов
IWenerator
или генерировать коды операций MSIL, используя метод
IWenerator. Emit.
Метод
IWenerator.Emit
в качестве единственного аргумента принимает поле члена класса
OpCodes,
непосредственно связанное с кодом операции MSIL.
В завершение вызываем метод
TypeBuilder.CreateType.
Это действие всегда должно выполняться последним, после того как вы определили члены нового типа. Далее мы получаем объект
Type
для нового типа с помощью метода
Type.GetType.
Этот объект будет храниться в члене-переменной клиентского приложения, которому он впоследствии понадобится.
Теперь все, что осталось сделать клиенту, — это получить член
Type
класса
CodeGenerator,
создать экземпляр активатора, экземпляр объекта
Methodlnfo
из типа и затем вызвать метод. Вот код, выполняющий эти действия, к которому добавлена небольшая проверка на наличие ошибок, чтобы быть уверенным, что все работает как надо:
using System;
using System.Reflection;
using ILGenServer;
public class ILGenClientApp {
public static void Main() {
Console.WriteLine("Calling DLL function to generate " + "a new type and method in memory..."); CodeGenerator gen = new CodeGeneratorQ;
Console.WriteLine("Retrieving dynamically generated
type..."); Type t = gen.T; if (null != t) {
Console.WriteLine("Instantiating the new type...");
object о = Activator.Createlnstance(t);
Console.WriteLine("Retrieving the type's " +
"HelloWorld method...");
Methodlnfo helloWorld = t.GetMethod("HelloWorld"); if (null != helloWorld) {
Console.WriteLine("Invoking our dynamically " +
"created HelloWorld method..."); helloWorld.Invoke(o, null); }
else <
Console.WriteLine("Could not locate " + "HelloWorld method"); } >
else {
Console.WriteLin&("Could not access Type from server"); > > >
Скомпоновав и исполнив это приложение, вы увидите:
Calling DLL function to generate a new type and method in memory...
Retrieving dynamically generated type...
Instantiating the new type...
Retrieving the type's HelloWorld method...
Invoking our dynamically created HelloWorld method...
Hello World
Хотя большинство приложений в этой книге состоят из одного модуля, вы можете создавать сборки, состоящие из нескольких модулей. Получать имена модулей из объекта
Assembly
можно двумя способами. Первый — это запрос массива всех модулей. При этом осуществляется проход по всем модулям и вывод любых нужных данных. Второй способ — получение информации о конкретном модуле.
Чтобы проиллюстрировать циклический опрос модулей, нужно создать сборку, состоящую из более чем одного модуля. Я сделаю это путем перевода
Get Assembly Name
в собственный класс и размещения этого класса в отдельном файле с именем
Assembly Utils.netmodule,
например, так: using System.Diagnostics;
namespace MyUtilities {
public class AssemblyUtils {
public static string GetAssembiyName(string[] args) {
string assemblyName;
if (0 == args.Length) <
Process p = Process.GetCurrentProcessQ;
assemblyName = p.ProcessName + ".exe"; } else
assemblyName = args[0];
ч
return assemblyName; } } }
После этого создаем модуль netmodule командой: esc /target:module AssemblyUtils.es
Переключатель
/target-.module
затавляет компилятор генерировать модуль, который позже будет включен в сборку. Приведенная команда создаст файл AssemblyUtils.netmodule. Подробнее о создании сборок и модулей см. главу 18.
А сейчас я хочу создать вспомогательный модуль, чтобы у нас был предмет для отражения. Приведенное ниже приложение будет использовать класс
AssemblyUtils.
Обратите внимание на оператор
using,
в котором указано пространство имен
My Utilities.
using System;
using System.Reflection;
using MyUtilities;
class GetModulesApp {
public static void Hain(string[] args)
{
string assemblyNarae = AssemblyUtils.GetAssemblyName(args);
Console.WriteLine("Loading info for " + assemblyName); Assembly a = Assembly.LoadFrom(assemblyName);
Module[] modules = a.GetModulesO; foreach(Module m in modules) <
Console.WriteLine("Module: " + m.Name); } } }
Чтобы скомпилировать это приложение и добавить к сборке модуль AssemblyUtils.netmodule, нужно задействовать переключатели командной строки:
esc /addmodule:AssemblyUtils.netmodule GetModules.es
Теперь у нас есть сборка из двух модулей. Чтобы увидеть это, запустите приложение. При этом получатся такие результаты:
Loading info for GetModulesApp.exe
Module: GetModulesApp.exe
Module: AssemblyUtils.netmodule
Как стало ясно из кода, я просто создал экземпляр объекта
Assembly
и вызвал его метод
GetModules.
Затем я циклически обработал возвращенный массив и вывел имя каждого из них.