1. Клиент направляет запрос на сервер.

2. Поток сервера принимает запрос и пересылает его потоку из пула для выполнения работы.

3. Поток из пула принимает клиентский запрос и выполняет доверенный код, то есть код, созданный в компании, в которой создано и протестировано само хост-приложение.

4. Доверенный код входит в блок t пу и вызывает из него другой домен (используя тип, производный от ManshalByRefObject). Этот домен содержит код (например, хранимую процедуру), созданный и протестированный сторонними разработчиками.

5. Хост фиксирует время получения исходного клиентского запроса. Если сторонний код не отвечает клиенту за определенное администратором время, хост вызывает метод Abort типа Thread, требуя от CLR остановить поток из пула и вынуждая выдать исключение ThreadAbortException.

6. На этом этапе поток пула начинает завершение, вызывая блоки finally, чтобы выполнить код очистки. В итоге поток возвращается в домен. Так как программа- заглушка вызвала сторонний код из блока try, в ней имеется и блок catch, который перехватывает исключение ThreadAbortException.

7. В ответ на перехват исключения ThreadAbortException хост вызывает метод ResetAbort типа Thread. Зачем это нужно, я объясню чуть позже.

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


 

Проясню некоторые непонятные места этой архитектуры. Во-первых, метод Abort типа Thread выполняется асинхронно. Он отмечает целевой поток флагом AbortRequested и немедленно возвращает управление. Обнаружив завершение потока, исполняющая среда пытается перенести этот поток в безопасное место (safe place). Исполняющая среда считает, что поток находится в безопасном месте, если его можно (по ее мнению) остановить без риска серьезных последствий. Поток находится в безопасном месте, если выполняет управляемую операцию блокировки, например бездействует или «спит». Перемещение в безопасное место осуществляется путем захвата (см. главу 21). Поток не считается находящимся в безопасном месте, если он выполняет конструктор класса типа, код блока catch или finally, код в критической области или неуправляемый код.

Как только поток оказывается в безопасном месте, исполняющая среда обнаруживает у него флаг AbortRequested и заставляет его выдать исключение ThreadAbortException. Если исключение не перехватывается, оно остается необработанным, выполняются все блоки finally и поток корректно завершается. В отличие от всех прочих, оставшись необработанным, исключение ThreadAbortException не приводит к остановке приложения. Исполняющая среда поглощает его, и поток завершается, но приложение и все оставшиеся его потоки продолжают работу.

В рассматриваемом примере хост перехватывает исключение ThneadAbont- Exception, получая возможность снова получить контроль над потоком и возвратить его в пул. Однако остается вопрос: что может помешать стороннему коду перехватить исключение ThneadAbontException, чтобы сохранить за собой контроль над потоком? У CLR к данному исключению особое отношение. Даже если код перехватывает исключение ThneadAbontException, CLR в конце блока catch автоматически повторно его генерирует.

В связи с этой особенностью CLR возникает вопрос: если CLR повторно генерирует исключение ThneadAbontException в конце блока catch, как же хосту удается перехватить это исключение и восстановить контроль над потоком? В блоке catch хоста есть вызов метода ResetAbont типа Thnead. Именно он запрещает CLR повторно генерировать исключение ThneadAbontException в конце каждого блока catch.

Тогда снова возникает вопрос: а что может запретить стороннему коду перехватить исключение ThneadAbontException и самому вызвать метод ResetAbont типа Thnead? К счастью, для вызова этого метода у вызывающей программы должно быть разрешение SecunityPenmission с флагом ContnolThnead, имеющим значение tnue. Создавая домен для кода сторонних разработчиков, хост не предоставляет такое разрешение, а, значит, такой код не сможет сохранить за собой контроль над его потоком.

Должен заметить, что брешь в системе безопасности в данном случае все-таки возможна: когда поток раскручивает исключение ThneadAbontException, сторонний код может выполнить блоки catch и finally, содержащие код с бесконечным циклом, не позволяющим хосту вернуть контроль над потоком. Эта проблема решается при помощи обсуждавшейся ранее политики расширения. Если останавливаемый поток не завершается за разумное время, CLR может перейти от корректной к принудительной остановке, принудительной выгрузке домена, отключению CLR или уничтожению процесса. Следует также заметить, что сторонний код может перехватить исключение ThneadAbontException и вбросить в блоке catch какое-то другое исключение. Если оно перехватывается, в конце блока catch CLR автоматически повторно выдаст исключение ThneadAbontException.

Большинство сторонних программ не представляет угрозы — просто с точки зрения хоста они тратят на решение своей задачи слишком много времени. Обычно блоки catch и finally содержат очень немного кода, выполняемого быстро без каких-либо бесконечных циклов или «долгоиграющих» операций. Поэтому вряд ли вам потребуется политика расширения для возвращения управления потоком хосту.

Кстати, у класса Thnead есть два метода Abont: один без параметров, а второй с параметром Object, в котором можно передать любой объект. Перехватив исключение ThneadAbontException, код может запросить свое предназначенное только для чтения свойство ExceptionState, которое вернет объект, переданный в качестве параметра. Это позволяет потоку, вызвавшему метод Abont, передать дополнительную информацию коду, перехватившему исключение ThneadAbontException. Хост может использовать это для информирования собственного перехватывающего кода о причине остановки потока.

Глава 23. Загрузка сборок и отражение

В этой главе рассказано о том, как находить информацию о типах, создавать их экземпляры и обеспечивать доступ к их членам, несмотря на то что во время компиляции об этих типах ничего не известно. Сведения, приведенные в этой главе, обычно нужны для создания динамически расширяемых приложений, то есть таких, для которых одна компания создает хост-приложение, а другие — подключаемые компоненты (add-ins), которые расширяют функциональность хоста. Тестировать совместную работу хоста и подключаемых компонентов невозможно, так как последние создаются разными компаниями, причем, как правило, уже после выпуска хост-приложения. Вот почему хосту приходится самостоятельно находить подключаемые компоненты во время выполнения.

Динамически расширяемое приложение может использовать хостинг CLR и домены приложений, как описано в главе 22. Хост выполняет код подключаемых компонентов в отдельных доменах приложений с собственными параметрами защиты и конфигурирования. Хост также может выгрузить подключаемый компонент, выгрузив домен приложений, в котором он выполняется. В конце главы мы поговорим о том, как задействовать все эти механизмы, — включая хостинг CLR, домены приложений, загрузку сборок, обнаружение типов, создание экземпляров типов и отражение, — для создания надежного, безопасного и динамически расширяемого приложения.

ПРИМЕЧАНИЕ

В .NET Framework версии 4.5 компания Microsoft ввела новый API отражения. У старого API было много недостатков. Например, он плохо поддерживал LINQ, встроенные политики были некорректными для некоторых языков, иногда он принудительно загружал ненужные сборки, в целом был слишком сложным и предлагал решения дял задач, которые крайне редко встречались на практике. В новом API все эти недостатки устранены. С другой стороны, в .NET 4.5 новый API отражения обладает меньшей полнотой, чем старый API. С новым API и некоторыми методами расширения (из класса System.Reflection.RuntimeReflectionExtensions) можно сделать все необходимое. В будущих версиях .NET Framework в новый API будут включены дополнительные методы.

Конечно, для настольных приложений старый API продолжает поддерживаться, так что перекомпиляция не нарушит совместимости существующего кода. Тем не менее в разработках, ориентированных на будущее, рекомендуется использовать новый API, поэтому он будет подробно рассматриваться в этой главе. В приложениях Windows Store (в которых проблема совместимости отсутствует) использование нового API обязательно.

Загрузка сборок

Как вы уже знаете, когда JIT-компилятор генерирует IL-код метода, он «смотрит», на какие типы есть ссылки в IL-коде. Далее во время выполнения JIT-компилятор по таблицам метаданных TypeRef и AssemblyRef выясняет, в какой сборке определен упоминаемый тип. Запись таблицы AssemblyRef содержит все части строгого имени сборки. JIT-компилятор собирает все эти части — имя (без расширения и пути), версию, региональные стандарты и открытый ключ — в строку, а затем пытается загрузить сборку с таким именем в домен приложений (если она еще не загружена). Если загружается сборка с нестрогим именем, идентификационная информация представляет собой только имя сборки (без версии, региональных стандартов и открытого ключа).

CLR пытается загрузить эту сборку, используя статический метод Load класса System.Reflection.Assembly. Этот метод описан в открытой документации, его можно вызывать для явной загрузки сборки в свои приложения. Он представляет собой CLR-эквивалент Win32^yHKHini LoadLibrary. Метод Load класса Assembly существует в нескольких перегруженных версиях. Вот прототипы наиболее популярных из них:

public class Assembly {

public static Assembly Load(AssemblyName assemblyRef);

public static Assembly Load(String assemblystring);

// Менее популярные перегруженные версии не показаны

}

Внутренний код Load заставляет CLR применить к сборке политику привязки версии с перенаправлением и ищет нужную сборку сначала в глобальном кэше сборок (GAC), а затем последовательно в базовом каталоге приложения, каталогах закрытых путей и каталоге, указанном в элементе CodeBase конфигурационного файла. Если методу Load передается сборка с нестрогим именем, он не применяет к ней политику, и CLR не ищет ее в GAC. Найдя искомую сборку, Load возвращает ссылку на объект Assembly, представляющий загруженную сборку. Если указанная сборка не найдена, появляется исключение System .10. FileNotFoundException.

ПРИМЕЧАНИЕ

В чрезвычайно редких ситуациях может потребоваться загрузить сборку, скомпонованную для определенной версии Microsoft Windows. В этом случае при определении идентификационной информации сборки можно указать сведения об архитектуре процесса. Например, если в GACхранятся нейтральная и специализированная (х86) версии сборки, CLR предпочтет специализированную версию (см. главу 3). Однако можно заставить CLR загрузить нейтральную версию, передав в метод Load класса Assembly такую строку:

"SomeAssembly, Version=2.0.0.0j Culture=neutral, PublicKeyToken=01234567890abcdej ProcessorArchitecture=MSIL"

На момент написания этой книги CLR поддерживает четыре возможных значения параметра ProcessorArchitecture: MSIL (Microsoft IL), х86, IA64 и AMD64.

ВНИМАНИЕ

Метод Load есть и у объекта System.AppDomain. В отличие от одноименного метода объекта Assembly, он является экземплярным методом, позволяющим загружать сборку в домен приложений. Этот метод создан для неуправляемого кода, позволяя хосту загрузить сборку в определенный домен приложений. Разработчикам управляемого кода лучше его избегать, и вот почему. При вызове методу Load объекта AppDomain передается строка, идентифицирующая сборку. Этот метод затем применяет политику и ищет сборку в обычных местах: на пользовательском жестком диске или в базовом каталоге. Вспомните, что с каждым доменом приложений связаны параметры конфигурации, определяющие правила поиска сборки для CLR. Так вот, при загрузке сборки CLR будет руководствоваться конфигурацией заданного, а не вызывающего домена приложений.

Однако метод Load объекта AppDomain возвращает ссылку на сборку. В силу того, что класс System.Assembly не является потомком System.MarshalByRefObject, объект сборки возвращается вызывающему домену приложений путем продвижения по значению. Но теперь для поиска и загрузки сборки CLR задействует параметры вызывающего домена приложений. Если сборку не удается найти при помощи политики вызывающего домена приложений или в заданных им каталогах поиска, генерируется исключение FileNotFoundException. Такая ситуация обычно нежелательна, поэтому следует избегать метода Load объекта System.AppDomain.

В большинстве динамически расширяемых приложений метод Load объекта AppDomain является предпочтительным механизмом загрузки сборки в домен приложений, но он требует наличия всех частей, идентифицирующих сборку. Часто разработчики создают инструменты или утилиты (такие, как ILDasm.exe, PEVerify. ехе, CorFlags.exe, GACUtil.exe, SGen.exe, SN.exe и XSD.exe), которые определенным образом обрабатывают сборку. Все они принимают параметр командной строки, задающий путь (с расширением) к файлу сборки.

Чтобы загрузить сборку с указанием пути, вызовите метод Load From класса Assembly:

public class Assembly {

public static Assembly LoadFrom(String path);

// Менее популярные перегруженные версии не показаны

}

Код LoadFrom сначала вызывает метод GetAssemblyName класса System. Reflection .AssemblyName, который открывает указанный файл, находит запись таблицы метаданных AssemblyRef, извлекает идентификационную информацию сборки и возвращает ее в объекте System. Reflection .AssemblyName (файл при этом закрывается). Затем LoadFrom вызывает метод Load класса Assembly, передавая ему объект AssemblyName. На этом этапе CLR применяет политику перенаправления версий и ищет в определенных местах соответствующую сборку. Найдя сборку, Load загружает ее и возвращает объект Assembly, представляющий загруженную сборку; именно его возвращает LoadFrom. Если методу Load не удается найти сборку, LoadFrom загружает сборку по пути, переданному в качестве параметра в LoadFrom.

Ясно, что если сборка с теми же идентификационными данными уже загружена, Load From просто возвращает объект Assembly, представляющий уже загруженную сборку.

Кстати, методу Load From можно передать в качестве параметра URL-адрес: Assembly а = Assembly.LoadFrom(@"http://Wintellect.com/SomeAssembly.dll");

При получении URL-адреса среда CLR загружает файл, устанавливает его в загрузочный кэш пользователя и уже из него загружает файл. Система должна быть подключена к Интернету, иначе произойдет исключение. Однако если файл уже был загружен в кэш ранее, а браузер Internet Explorer настроен на работу в автономном режиме (команда Work Offline (Работать автономно) в меню File (Файл)), будет использован файл из кэша, и исключение не возникнет. Также можно вызвать метод UnsafeLoadFrom, который загрузит уже загруженную веб-сборку, игнорируя некоторые параметры защиты.

ВНИМАНИЕ

Fla одной машине могут находиться разные сборки с одинаковой идентификационной информацией. Так как Load From вызывает Load, может оказаться, htoCLR загрузит не указанный, а другой файл, что чревато непредсказуемым поведением. Ь1астоятель- но рекомендуется при каждой компоновке сборки изменять номер редакции — так обеспечивается строгая индивидуальность идентификационной информации всех сборок, а значит, вызов метода LoadFrom не принесет неожиданностей.

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

Если вы создаете инструмент, который просто анализирует метаданные сборки с использованием отражения (об этом — чуть позже), не выполняя никакого кода сборки, лучше всего для загрузки сборки задействовать метод ReflectionOnlyLoadFrom или, в некоторых редких случаях, метод Ref lectionOnly Load класса Assembly. Вот прототипы обоих методов:

public class Assembly {

public static Assembly ReflectionOnlyl_oadFrom(String assemblyFile);

public static Assembly ReflectionOnlyLoad(String assemblystring);

// Менее популярные перегруженные версии не показаны

}

Метод ReflectionOnlyLoadFrom загружает указанный файл, не получая информацию строгого имени сборки и не выполняя поиск файла в GAC или где-либо еще. Метод ReflectionOnlyLoad выполняет поиск указанной сборки в GAC, базовом каталоге приложения, частных каталогах и каталоге, указанном в элементе CodeBase. Однако в отличие от Load этот метод не применяет политику версий, поэтому не предоставляет гарантий, что будет загружена именно та сборка, которая ожидалась. Если вы хотите самостоятельно применить политику версий к сборке, можно передать строку с идентификационной информацией в метод AppDomain класса ApplyPolicy.

При загрузке сборок методом ReflectionOnlyLoadFrom или ReflectionOnlyLoad

среда CLR запрещает выполнение какого-либо кода сборки, а при попытке выполнить код генерирует исключение InvalidOperationException. Эти методы позволяют инструменту загружать сборки с отложенным подписанием, сборки для процессора другой архитектуры, а также сборки, для загрузки которых нужны особые разрешения.

Часто при использовании отражения для анализа сборки, загруженной одним из указанных двух методов, код должен зарегистрировать метод обратного вызова на событие Ref lectionOnlyAssemblyResolve класса AppDomain, чтобы вручную загружать произвольные сборки, задаваемые клиентом (при необходимости вызывая метод ApplyPolicy класса AppDomain); CLR не делает этого автоматически. Будучи вызванным, метод обратного вызова должен вызвать метод ReflectionOnlyLoadFrom или ReflectionOnlyLoad класса Assembly, чтобы явно загрузить указанную сборку и вернуть ссылку на нее.

ПРИМЕЧАНИЕ

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

Казалось бы, сборки, загруженные методом ReflectionOnlyLoadFrom или ReflectionOnlyLoad, должно быть разрешено выгрузить. В конце концов, ведь код этих сборок нельзя выполнять. Однако CLR не разрешает выгрузку сборок, загруженных одним из этих методов, по той простой причине, что после загрузки сборок вы всегда сможете использовать отражение для создания объектов, ссылающихся на метаданные, определенные в этих сборках. При выгрузке сборки потребовалось бы каким-то образом сделать объекты недействительными, но отслеживание всех этих связей — слишком сложная и ресурсоемкая задача.

Многие приложения содержат ЕХЕ-файлы, зависимые от многих DLL-файлов. При установке этих приложений также должны устанавливаться все файлы. Однако обычно практикуется установка единственного ЕХЕ-файла. В этом случае

в первую очередь идентифицируйте все DLL-файлы, от которых зависит ваш ЕХЕ-файл и которые не являются частью платформы Microsoft .NET Framework. Затем добавьте эти DLL-файлы к вашему проекту в Visual Studio и для каждого добавленного DLL-файла откройте окно свойств и измените значение Build Action на Embedded Resource. Это действие даст указание компилятору C# добавить DLL- файлы в ЕХЕ-файл, который в конечном итоге и будет устанавливаться.

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

private static Assembly ResolveEventHandler(Object sender, ResolveEventArgs args) { String dllName = new AssemblyName(args.Name).Name + ".dll";

var assem = Assembly.GetExecutingAssemblyQ;

String resourceName = assem.GetManifestResourceNames().FirstOrDefault(rn => rn.EndsWith(dllName));

if (resourceName == null) return null;

// Not found, maybe another handler will find it using (var stream = assem.GetManifestResourceStream(resourceName)) {

Byte[] assemblyData = new Byte[stream.Length]; stream.Read(assemblyData, 0, assemblyData.Length); return Assembly.Load(assemblyData);

}

}

При первом вызове в потоке метода, ссылающегося на тип, зависящий от DLL-файла, возникнет событие AssemblyResolve, и показанный программный

код обратного вызова найдет встроенный DLL-файл и загрузит его путем вызова перегруженного метода Load, у которого в качестве аргумента будет использоваться Byte[ ]. И хотя мне нравится прием встраивания зависимых DLL в другие сборки, следует помнить, что он увеличивает объем памяти, используемой приложением во время выполнения.

Использование отражения для создания динамически расширяемых приложений

Как вам известно, метаданные — это набор таблиц. При построении сборки или модуля компилятор создает таблицы определений типов, полей, методов и т. д. В пространстве имен System. Reflection есть несколько типов, позволяющих писать код разбора этих таблиц. На самом деле типы из этого пространства имен предоставляют объектную модель для работы с метаданными сборки или модуля.

Типы, составляющие эту объектную модель, позволяют легко перечислить все типы из таблицы определений типов, а также получить для каждого из них базовый тип, интерфейсы и ассоциированные с ним флаги. Остальные типы из пространства имен System.Reflection дают возможность запрашивать поля, методы, свойства и события типа путем разбора соответствующих таблиц метаданных. Можно узнать, какими атрибутами (см. главу 18) помечена та или иная сущность метаданных. Есть даже классы, позволяющие определить указанные сборки и методы и возвращающие в методе байтовый IL-поток. Располагая этой информацией, можно легко создавать программы, сходные с программой ILDasm.exe компании Microsoft.

ПРИМЕЧАНИЕ

Нужно иметь в виду, что некоторые типы отражения и часть их членов созданы специально для разработчиков, пишущих компиляторы для CLR. Прикладные разработчики обычно не используют эти типы и члены. В документации к библиотеке FCL не сказано четко, какие типы предназначены для разработчиков компиляторов, а какие — для разработчиков приложений, но если понимать, что некоторые типы и члены отражения предназначены «не для всех», то документация становится более понятной.

В реальности приложениям редко требуются типы отражения. Обычно отражение используется в библиотеках классов, которым нужно понять определение типа для предоставления расширенной функциональности. Например, механизм сериализации из FCL (см. главу 24) применяет отражение, чтобы выяснить, какие поля определены в типе. Объект форматирования из механизма сериализации получает значения этих полей и записывает их в поток байтов для пересылки по Интернету, сохранения в файле или отправки в буфер обмена. Проектировщики Microsoft Visual Studio используют отражение, чтобы определить, какие свойства следует показывать разработчикам при размещении элементов на поверхности веб-формы или формы Windows Forms во время ее создания.

Отражение также применяют, когда для решения некоторой задачи во время выполнения приложению нужно загрузить определенный тип из некоторой сборки. Например, приложение может попросить пользователя предоставить имя сборки и типа, чтобы явно загрузить ее, создать экземпляр данного типа и вызывать его методы. Концептуально подобное использование отражения напоминает вызов Шт32-функций LoadLibrary и GetProcAddress. Часто привязку к типам и вызываемым методам, осуществляемую таким образом, называют поздним связыванием (late binding) — в отличие от раннего связывания (early binding), применяемого, когда требуемые приложению типы и методы известны при компиляции.

Производительность отражения

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

□ При использовании отражения безопасность типов на этапе компиляции не контролируется. Так как в отражении активно применяются строки, вы теряете безопасность типов на этапе компиляции. Допустим, для поиска типа с именем int средствами отражения используется вызов Type.GetType( "int"); код успешно компилируется, но во время выполнения возвращает null, так как с точки зрения СЬЯтип int называется System. Int32.

□ Отражение работает медленно. При использовании отражения имена типов и их члены на момент компиляции не известны — они определяются в процессе выполнения, причем все типы и члены идентифицируются по строковому имени. Это значит, что при отражении постоянно выполняется поиск строк в метаданных сборки пространства имен System. Reflection. Часто строковый поиск выполняется без учета регистра, что дополнительно замедляет процесс.

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

В силу этих причин лучик; не использовать отражение для обращения к полям или вызова методов/свойств. Если вы пишете приложение, которое динамически ищет и создает объекты, следуйте одному из перечисленных далее подходов.

□ Порождайте свои типы от базового типа, известного на момент компиляции. Затем, создав экземпляр своего типа во время выполнения, поместите ссылку на него в переменную базового типа (выполнив приведение типа) и вызывайте виртуальные методы базового типа.

□ Реализуйте в типах интерфейсы, известные на момент компиляции. Затем, создав экземпляр своего типа во время выполнения, поместите ссылку на него в переменную того же типа, что и интерфейс (выполнив приведение типа), и вызывайте методы, определенные в интерфейсе.

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

В любом случае я настоятельно рекомендую определять базовый тип или интерфейс в их собственной сборке — будет меньше проблем с управлением версиями. Подробнее об этом см. раздел «Создание приложений с поддержкой подключаемых компонентов».

Нахождение типов, определенных в сборке

Отражение часто используется, чтобы выяснить, какие типы определены в сборке. Для получения этой информации FCL предлагает несколько методов. Наиболее популярный — свойство ExpontedTypes класса Assembly. Вот пример кода, который загружает сборку и выводит имена всех определенных в ней открытых экспортированных типов:

using System;

using System.Reflection;

public static class Program { public static void Main() {

String dataAssembly = "System.Data, version=4.0.0.0, " +

"culture=neutral, PublicKeyToken=b77a5c561934e089";

LoadAssemAndShowPublicTypes(dataAssembly);

}

private static void LoadAssemAndShowPublicTypes(String assemld) {

// Явная загрузка сборки в домен приложений Assembly а = Assembly.Load(assemld);

// Цикл выполняется для каждого типа,

// открыто экспортируемого загруженной сборкой foreach (Type t in a.ExportedTypes) {

// Вывод полного имени типа Console.Write Line(t.FullName);

}

}

}

Объект Type

Приведенный выше код перебирает массив объектов System.Туре. Тип System. Туре — отправная точка для операций с типами и объектами. Он представляет ссылку на тип (в отличие от определения типа).

Как вы помните, в System.Object определен открытый невиртуальный метод GetType. При вызове этого метода CLR определит тип указанного объекта и вернет ссылку на его объект Туре. Поскольку для каждого типа в домене приложений есть только один объект Туре, чтобы выяснить, относятся ли объекты к одному типу, можно задействовать операторы равенства и неравенства:

private static Boolean AreObjectsTheSameType(Object ol, Object o2) { return ol.GetTypeQ == o2.GetType();

}

Помимо вызова метода GetType класса Object FCL предлагает другие способы получения объекта Туре:

□ В типе System. Туре есть несколько перегруженных версий статического метода GetType. Все они принимают тип String. Эта строка должна содержать полное

имя типа (включая его пространства имен). Имена примитивных типов, поддерживаемые компилятором (такие, как int, string, bool и другие типы языка С#), запрещены, потому что они ничего не значат для CLR. Если строка содержит просто имя типа, метод проверяет, определен ли тип с указанным именем в вызывающей сборке. Если это так, возвращается ссылка на соответствующий объект Туре.

□ Если в вызывающей сборке указанный тип не определен, проверяются типы, определенные в MSCorLib.dll. Если и после этого тип с указанным именем найти не удается, возвращается null или генерируется исключение System. TypeLoadException — все зависит от того, какая перегруженная версия метода GetType вызывалась и какие ей передавались параметры. В документации на FCL есть исчерпывающее описание этого метода.

В GetType можно передать полное имя типа с указанием сборки, например:

"System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral,

PublicKeyToken=b77a5c561934e089"

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

□ В типе System .Туре есть статический метод Ref lectionOnlyGetType. Этот метод ведет себя так же, как только что описанный метод GetType, за исключением того, что тип загружается только для отражения, но не для выполнения кода.

□ В типе System. Typelnfo есть экземплярные методы DeclaredNestedTypes и GetDeclaredNestedTypes.

□ В типе System. Reflection .Assembly есть экземплярные методы GetType, De- finedTypes и ExportedTypes.

ПРИМЕЧАНИЕ

Microsoft использует нотацию Бэкуса-Наурадля записи имен типов и имен с указанием сборки, которые используются для написания строк, передаваемых в методы отражения. Знание нотации оказывается очень кстати при использовании отражения и особенно при работе с вложенными типами, обобщенными типами и методами, ссылочными параметрами или массивами. За полным описанием нотации обращайтесь к документации FCI-ИЛИ выполните поиск в Интернете по строке «Backus-Naur Form Grammar for Type Names». Вы также можете посмотреть методы MakeArrayType, MakeByRefType, MakeGenericType и MakePointerType классов Type и Typelnfo.

Во многих языках программирования есть оператор, позволяющий получить объект Туре по имени типа. Для получения ссылки на Туре лучше использовать именно такой оператор, а не перечисленные методы, так как при компиляции оператора получается более быстрый код. В C# это оператор typeof, хотя обычно его не применяют для сравнения информации о типах, загруженных посредством позднего и раннего связывания, как в следующем примере: private static void SomeMethod(Object о) {

// GetType возвращает тип объекта во время выполнения // (позднее связывание)

// typeof возвращает тип указанного класса // (раннее связывание)

if (o.GetType() == typeof(Filelnfo)) { }

if (o.GetType() == typeof(Directorylnfo)) { ... }

}

ПРИМЕЧАНИЕ

Первая инструкция if проверяет, ссылается ли переменная о на объект типа Filelnfo, но не на тип, производный от Filelnfo. Иначе говоря, этот код проверяет на точное, а не на совместимое соответствие. Совместимое соответствие обычно достигается путем приведения типов либо использования оператора is или as языка С#.

Как упоминалось ранее, объект Туре представляет ссылку на тип, то есть содержит минимум информации. Для получения более полной информации о типе следует получить объект Typelnfo, представляющий определение типа. Объект Туре можно преобразовать в Typelnfo вызовом метода расширения GetTypelnfo класса System.Reflection.IntrospectionExtensions:

Type typeReference = 11 Например: o.GetType() или typeof(Object)

Typelnfo typeDefinition = typeReference.GetTypeInfo();

И хотя эта возможность менее полезна, объект Typelnfo можно преобразовать в объект Туре вызовом метода AsType класса Typelnfo.

Typelnfo typeDefinition =

Type typeReference = typeDefinition.AsType();

При получении объекта Typelnfo CLR приходится убеждаться в том, что сборка, определяющая тип, загружена. Это затратная операция, без которой можно обойтись, если вам достаточно ссылок на типы (объекты Туре). Однако после получения объекта Typelnfo можно запрашивать многие свойства типа и узнать о них много полезного. Большинство свойств, таких как IsPublic, IsSealed, IsAbstract, IsClass, IsValueType и т. д., описывают флаги, связанные с типом. Другие свойства (к ним относятся Assembly, AssemblyQualifiedName, FullName, Module и пр.) возвращают имя сборки, в которой определен тип или модуль, и полное имя типа. При помощи свойства BaseType можно узнать базовый тип. Все методы и свойства, предоставляемые типом Typelnfo, описаны в документации FCL.

Создание иерархии типов, производных от Exception

В приложении-примере ExceptionTree (исходный текст см. далее) описанные концепции используются, чтобы загрузить в домен приложений определенное подмножество сборок и показать все типы, которые в конечном итоге наследуют от типа System. Exception. Кстати, это программа, которую я написал, чтобы создать иерархию исключений, приведенную в главе 20.

public static void Go() {

// Явная загрузка сборок для отражения LoadAssembliesQ;

// Фильтрация и сортировка всех типов var allTypes =

(from a in AppDomain.CurrentDomain.GetAssemblies() from t in a.ExportedTypes

where typeof(Exception).GetTypeInfo().IsAssignableFrom(t.GetTypeInfo()) orderby t.Name select t).ToArray();

II Построение и вывод иерархического дерева наследования Console.Write Line(WalklnheritanceHierarchy(new StringBuilder(), 0, typeof(Exception),

allTypes));

private static StringBuilder WalkInheritanceHierarchy(

StringBuilder sb, Int32 indent, Type baseType, IEnumerable<Type> allTypes) { String spaces = new StringC indent * 3); sb.AppendLine(spaces + baseType.FullName); foreach (var t in allTypes) {

if (t.GetTypelnfoQ .BaseType != baseType) continue; WalkInheritanceHierarchy(sb, indent + 1, t, allTypes);

}

return sb;

private static void LoadAssemblies() { String[] assemblies = {

"System,

"System.Core,

"System.Data,

"System.Design,

"System.DirectoryServices, "System.Drawing,

"System.Drawing.Design,

"System.Management,

"System.Messaging,

"System.Runtime.Remoting, "System.Security,

"System.ServiceProcess, "System.Web,

"System.Web.RegularExpressions, PublicKeyToken={l}“, "System.Web.Services, PublicKeyToken={l}“,

"System.Xml, PublicKeyToken={0}",

String EcmaPublicKeyToken = "Ь77а5с5б1934е089"; String MSPublicKeyToken = "b03f5f7flld50a3a";

// Получение версии сборки, содержащей System.Object.

// Этот же номер версии предполагается для всех остальных сборок.

Version version = typeof(System.Object).Assembly.GetNameQ.Version;

// Явная загрузка сборок foreach (String a in assemblies) {

String Assemblyldentity =

String.Format(aj EcmaPublicKeyToken, MSPublicKeyToken) +

", Culture=neutralj Version=" + version;

Assembly.Load(Assemblyldentity);

}

}

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

Получив ссылку на объект, производный от Туре, можно создать экземпляр этого

типа. FCL предлагает для этого несколько механизмов.

□ Методы Create Instance класса System.Activator. Этот класс поддерживает несколько перегруженных версий статического метода Createlnstance. При вызове этому методу передается ссылка на объект Туре либо значение String, идентифицирующее тип объекта, который нужно создать. Версии, принимающие тип Туре, проще: вы передаете методу набор аргументов конструктора, а он возвращает ссылку на новый объект.

Версии Createlnstance, в которых желаемый тип задают строкой, чуть сложнее. Во-первых, для них нужна еще и строка, идентифицирующая сборку, в которой определен тип. Во-вторых, эти методы позволяют создавать удаленные объекты, если правильно настроить параметры удаленного доступа. В-третьих, вместо ссылки на новый объект эти версии метода возвращают объект System.Runtime.Remoting.ObjectHandle (производный от System. MarshalByRefObject).

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

□ Методы CreatelnstanceFrom объекта System.Activator. Класс Activator также поддерживает несколько статических методов CreatelnstanceFrom. Они не отличаются от Createlnstance за исключением того, что для них всегда нужно задавать строковыми параметрами тип и сборку, в которой он находится. Заданная сборка загружается в вызывающий домен приложений методом Load From (а не Load) объекта Assembly. Поскольку ни один из методов CreatelnstanceFrom не принимает параметр Туре, все они возвращают ссылку на тип ObjectHandle, который необходимо дополнительно развернуть.

□ Методы объекта System.AppDomain. Тип AppDomain поддерживает четыре экземплярных метода (у каждого есть несколько перегруженных версий), создающих экземпляр типа: Cneatelnstance, CneatelnstanceAndUnwnap, Cneate- IntanceFnom и CneatelnstanceFnomAndUnwnap. Они работают совсем как методы Activator, но являются экземплярными методами, позволяя задать домен приложений, в котором нужно создать объект. Методы, названия которых оканчиваются на Unwrap, удобнее, так как они позволяют обойтись без дополнительного вызова метода.

□ Экземплярный метод Invoke объекта System.Reflection.Constructorlnfo.

При помощи ссылки на объект Typelnfo можно привязаться к некоторому конструктору и получить ссылку на объект Constructorlnfo, чтобы затем вызвать его метод Invoke. Новый объект всегда создается в вызывающем домене приложений, а затем возвращается ссылка на новый объект. К этому методу мы тоже вернемся позднее в этой главе.

ПРИМЕЧАНИЕ

Среда CLR не требует, чтобы у значимого типа был конструктор. И это создает проблемы, так как все перечисленные механизмы создают объект путем вызова его конструктора. Однако версии метода Createlnstance типа Activator позволяют создавать экземпляры значимых типов без вызова их конструкторов. Чтобы создать экземпляр значимого типа, не вызывая его конструктор, нужно вызвать версию Createlnstance, принимающую единственный параметр Туре, или версию, принимающую параметры Туре и Boolean.

Эти механизмы позволяют создавать объекты любых типов, кроме массивов (то есть типов, производных от System. Array) и делегатов (потомков типа System. MulticastDelegate). Чтобы создать массив, надо вызвать статический метод Createlnstance объекта Array (существует несколько перегруженных версий этого метода). В первом параметре всех версий Createlnstance передается ссылка на объект Туре, описывающий тип элементов массива. Прочие параметры Createlnstance позволяют задавать размерность и границы массива. Для создания делегата следует вызвать статический метод CreateDelegate объекта Delegate (у этого метода также есть несколько перегруженных версий). Первый параметр любой версии CreateDelegate — это ссылка на объект Туре, описывающий тип делегата. Остальные параметры позволяют указать, для какого экземплярного метода объекта или для какого статического метода типа делегат должен служить оболочкой.

Для создания экземпляра обобщенного типа сначала нужно получить ссылку на открытый тип, а затем вызвать открытый экземплярный метод MakeGenericType объекта Туре, передав массив типов, который нужно использовать в качестве параметров типа. Затем надо получить возвращенный объект Туре и передать его в один из описанных ранее методов. Вот пример:

using System;

using System.Reflection;

internal sealed class Dictionary<TKey, TValue> { }

public static class Program { public static void Main() {

// Получаем ссылку на объект Туре обобщенного типа Type openType = typeof(Dictionary<,>);

// Закрываем обобщенный тип, используя TKey=String, TValue=Int32 Type closedType = openType.MakeGenericType( new Type[] { typeof(String), typeof(Int32) })j

// Создаем экземпляр закрытого типа

Object о = Activator.Createlnstance(closedType)j

// Проверяем, работает ли наше решение Console.WriteLine(o.GetType());

>

>

Скомпилировав и выполнив этот код, мы получим: Dictionary' 2[System.String,System.Int32]

Создание приложений с поддержкой подключаемых компонентов

В построении открытых расширяемых приложений центральное место занимают интерфейсы. Вместо них можно было бы задействовать базовые классы, но в общем случае интерфейс предпочтительнее, так как позволяет разработчику подключаемого компонента (add-in) выбрать собственные базовые классы. Допустим, вы хотите создать приложение, позволяющее пользователям создавать типы, которые ваше приложение сможет загружать и применять. Такое приложение должно строиться следующим образом.

□ Создайте сборку для хоста, определяющую интерфейс с методами, обеспечивающими взаимодействие вашего приложения с подключаемыми компонентами. Определяя параметры и возвращаемые значения методов этого интерфейса, постарайтесь задействовать другие интерфейсы или типы, определенные BMSCorLib.dll. Если нужно передавать и возвращать собственные типы данных, определите их в этой же сборке. Задав интерфейс, дайте сборке строгое имя (см. главу 3), после чего можете передать ее своим партнерам и пользователям. После публикации нужно избегать любых изменений типов сборки, которые могут нарушить работу подключаемых модулей. В частности, вообще нельзя изменять интерфейс. Но если вы определили типы данных, ничего не случится, если вы добавите в них новые члены. Внеся какие-либо изменения в сборку, нужно развертывать ее вместе с файлом политики издателя (см. главу 3).

ПРИМЕЧАНИЕ

Вы можете использовать типы, определенные в MSCorLib.dll: CLR всегда загружает ту версию MSCorLib.dll, которая соответствует версии самой среды CLR. Кроме того, в процесс всегда загружается только одна версия MSCorLib.dll. Иначе говоря, разные версии MSCorLib.dll никогда не загружаются совместно (см. главу 3). В итоге несоответствий версий типа не будет, и ваше приложение использует меньше памяти.

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

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

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

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

using System;

namespace Wintellect.HostSDK { public interface IAddln {

String DoSomething(Int32 x);

}

}

Затем идет код сборки подключаемого компонента — библиотеки AddlnTypes.dll, в которой определены два открытых типа, реализующие интерфейс HostSDK. Для построения этой сборки необходимо добавить ссылку на HostSDK.dll:

using System;

using Wintellect.HostSDK;

public sealed class AddlnA : IAddln { public AddlnAQ {

}

public String DoSomething(Int32 x) { return "Addln A: " + x.ToStringO;

>

>

public sealed class AddlnB : IAddln { public AddlnBQ {

}

public String DoSomething(Int32 x) {

return "AddlnB: " + (x * 2).ToString();

}

}

Третьим рассмотрим код сборки простого хоста (консольного приложения) — файла Host.exe. При построении этой сборки используется ссылка на HostSDK.dll. Определяя используемые подключаемым модулем типы, код хоста предполагает, что искомые типы определены в сборках, файлы которых содержат расширение dll, а сами сборки развернуты в том же каталоге, что и ЕХЕ-файл хоста. Библиотека Microsoft MEF (Managed Extensibility Framework) строится на базе различных механизмов, которые я демонстрирую в этом разделе, а также предоставляет дополнительные средства регистрации и поиска подключаемых компонентов. Если вы занимаетесь построением динамически расширяемых приложений, я рекомендую поближе познакомиться с MEF, потому что это упростит часть материала этой главы.

using System;

using System.10;

using System.Reflection;

using System.Collections.Generic;

using Wintellect.HostSDK;

public static class Program { public static void Main() {

// Находим каталог, содержащий файл Host.exe String AddlnDir =

Path.GetDirectoryName( Assembly.GetEntryAssembly().Location);

// Предполагается, что сборки подключаемых модулей

// находятся в одном каталоге с ЕХЕ-файлом хоста

var AddlnAssemblies = Directory.EnumerateFiles(AddInDir, "*.dll");

// Создание набора объектов Туре, которые могут // использоваться хостом var AddlnTypes =

from file in AddlnAssemblies let assembly = Assembly.Load(file)

from t in assembly.ExportedTypes // Открыто экспортируемые типы // Тип может использоваться, если это класс, реализующий IAddln where t.IsClass &&

typeof(IAddln).GetTypeInfo().IsAssignableFrom(t.GetTypeInfo())

select t;

// Инициализация завершена: хост обнаружил типы, пригодные для использования // Пример конструирования объектов подключаемых компонентов // и их использования хостом

foreach (Type t in AddlnTypes) {

IAddln ai = (IAddln) Activator.Createlnstance(t)j Console.Write Line(ai.DoSomething(S));

}

}

}

В этом простом примере не используются домены приложений. Однако в реальной жизни подключаемые модули создаются в собственных доменах приложений с собственными параметрами защиты и конфигурирования. И, конечно же, домен приложений можно выгрузить, если нужно удалить подключаемый модуль из памяти. Чтобы обеспечить взаимодействие между доменами приложений, лучше всего потребовать от разработчиков подключаемых модулей создавать собственные внутренние типы, производные от MarshalByRefOb ject. При создании нового домена приложений хост создаст в нем экземпляр собственного производного от MarshalByRefObject типа. Код хоста (в основном домене) будет взаимодействовать с собственным типом (в других доменах), заставляя их загружать сборки подключаемых модулей, создавая и используя экземпляры определенных в этих модулях типов.

Нахождение членов типа путем отражения

До настоящего момента я рассказывал о тех составляющих механизма отражения — загрузке сборок, нахождении типов и создании объектов, — которые необходимы для создания динамически расширяемых приложений. Однако, чтобы обеспечить высокую производительность и безопасность типов во время компиляции, нужно избегать отражения. В динамически расширяемом приложении после создания объекта код хоста обычно приводит объект к интерфейсному типу (это предпочтительный вариант) или базовому классу, известному на момент компиляции; это обеспечивает быстроту доступа к членам объекта и безопасность типов во время компиляции.

В оставшейся части этой главы я рассказываю о некоторых аспектах отражения, применяемых для нахождения и вызова членов типа. Возможность поиска и вызова членов типа обычно нужна при создании инструментов для разработчиков и средств анализа сборок, ориентированных на выявление определенных структур в программном коде или использование определенных членов. В качестве примера таких инструментов приведу ILDasm, FxCop и конструкторы форм для приложений Windows Forms и Web Forms, разрабатываемых в Visual Studio. Также в некоторых библиотеках классов существует возможность нахождения и вызова членов типа для предоставления разработчикам расширенной функциональности. Пример — библиотеки, обеспечивающие сериализацию и десериализацию, а также простую привязку к данным.

Нахождение членов типа

Членами типа могут быть поля, конструкторы, методы, свойства, события и вложенные типы. В FCL есть тип System. Ref lection. Memberlnf о — абстрактный класс, инкапсулирующий набор свойств, общих для всех членов типа. У Memberlnf о много дочерних классов, каждый из которых инкапсулирует чуть больше свойств отдельных членов типа (рис. 23.1).

Рис. 23.1. Иерархия типов отражения


 

Приведенная далее программа демонстрирует, как нужно запрашивать члены типа и выводить информацию о них. Этот код обрабатывает все открытые типы всех сборок, загруженных в вызывающий домен приложений. Для каждого типа вызывается свойство DeclaredMembers, которое возвращает коллекцию объектов типа, производного от Memberlnf о; каждый объект описывает один член из определенных в типе. Далее для каждого члена выводятся его описание (поле, конструктор, метод, свойство и т. п.) и строковое значение (полученное вызовом ToString).

using System;

using System.Reflection;

public static class Program { public static void Main() {

// Перебор всех сборок, загруженных в домене

Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly a in assemblies) {

Show(0, "Assembly: {0}", а);

// Поиск типов в сборке foreach (Type t in a.ExportedTypes) { Show(l, "Type: {0}", t);

// Получение информации о членах типа

foreach (Memberlnfo ml in t.GetTypelnfoQ.DeclaredMembers) {

String typeName = String.Empty;

If (ml Is Type) typeName = "(Nested) Type";

If (ml Is Fieldlnfo) typeName = "Fieldlnfo";

If (ml Is Methodlnfo) typeName = "Methodlnfo";

If (ml Is Constructorlnfo) typeName = "Constructolnfo";

If (ml Is Propertylnfo) typeName = "Propertylnfo";

If (ml Is Eventlnfo) typeName = "Eventlnfo";

Show(2, "{0}: {1}", typeName, mi);

}

} }

}

private static void Show(Int32 indent, String format, params Object[] args) { Console.WriteLine(new String(' ', 3 * indent) + format, args);

}

}

После компиляции и запуска приложения мы получаем массу информации. Вот ее часть:

Assembly: mscorlib, Version=4.0.0.0, Culture=neutral,

PublicKeyToken=b77a5c561934e089 Type: System.Object

Methodlnfo: System. String ToStringQ Methodlnfo: Boolean Equals(System.Object)

Methodlnfo: Boolean Equals(System.Object, System.Object)

Methodlnfo: Boolean ReferenceEquals(System.Object, System.Object) Methodlnfo: Int32 GetHashCodeQ Methodlnfo: System. Type GetTypeQ Methodlnfo: Void Finalize()

Methodlnfo: System.Object MemberwiseCloneQ Methodlnfo: Void FieldSetter(System.String,

Methodlnfo: Void FieldGetter(System.String,

System.Object ByRef)

Methodlnfo: System.Reflection.Fieldlnfo GetFieldlnfo(System.String, System.String)

Constructolnfo: Void .ctor()

Type: System.Collections.Generic.IComparer'1[T]

Methodlnfo: Int32 Compare(T, T)

Type: System.Collections.IEnumerator Methodlnfo: Boolean MoveNextQ Methodlnfo: System.Object get_Current()

Methodlnfo: Void Reset()

Propertylnfo: System.Object Current Type: System.IDisposable

Methodlnfo: Void Dispose()

Type: System.Collections.Generic.IEnumerator'l[T]

Methodlnfo: T get_Current()

Propertylnfo: T Current
Type: System.ArraySegment'l[T]

Methodlnfo: T[] get_Array()

Methodlnfo: Int32 get_Offset()

Methodlnfo: Int32 get_Count()

Methodlnfo: Int32 GetHashCodeQ Methodlnfo: Boolean Equals(System.Object)

Methodlnfo: Boolean Equals(System.ArraySegment'1[T]) Methodlnfo: Boolean op_Equality(System.ArraySegment'l[T]j System.ArraySegment'1[T])

Methodlnfo: Boolean op_Inequality(System.ArraySegment'1[T], System.ArraySegment'1[T])

Constructolnfo: Void .ctor(T[])

Constructolnfo: Void .ctor(T[]j Int32j Int32)

Propertylnfo: T[] Array Propertylnfo: Int32 Offset Propertylnfo: Int32 Count Fieldlnfo: T[] array Fieldlnfo: Int32 offset

Так как тип Memberlnfo является корнем иерархии, стоит обсудить его подробнее. В табл. 23.1 показаны некоторые неизменяемые (только для чтения) свойства и методы типа Memberlnfo, общие для всех членов типа. Как вы помните, System. Туре наследует от типа Memberlnfo, поэтому Туре также обладает всеми перечисленными в таблице свойствами.

Таблица 23.1. Свойства и методы, общие для всех типов, производных

от Memberlnfo

Имя члена Тип члена Описание
Name Свойство String Возвращает имя члена
DeclaringType Свойство Туре Возвращает тип, объявляющий член
Module Свойство Module Возвращает модуль, объявляющий член
CustomAttributes Свойство, возвращающее I Enumer able< Custom Attri buteData> Возвращает коллекцию, каждый элемент которой идентифицирует экземпляр настраиваемого атрибута, которым помечен этот член. Такие атрибуты могут применяться к любому члену. И хотя тип Assembly не наследует от Memberlnfo, он предоставляет такое же свойство, которое может использоваться со сборками


 


 

Каждый элемент коллекции, возвращаемой DeclaredMembers, представляет собой ссылку на конкретный тип из этой иерархии. Помимо метода DeclaredMembers, воз

вращающего все члены типа, Typelnfo также поддерживает методы, возвращающие определенные разновидности членов: GetDeclaredNestedType, GetDeclaredField, GetDeclaredMethod, GetDeclaredProperty и GetDeclaredEvent. Все эти методы возвращают ссылку на объект Typelnfo, Fieldlnfо, Methodlnfо, Propertylnfо или Eventlnfo соответственно. Также существует метод GetDeclaredMethods, возвращающий коллекцию объектов Methodlnf о с описанием методов, соответствующих заданному строковому имени.

На рис. 23.2 представлена сводка типов, используемых приложениями для обхода модели объектов отражения. Домен приложений (AppDomain) дает возможность узнать, какие сборки в него загружены, сборка (Assembly) — из каких модулей она состоит, а сборка (Assembly) или модуль (Module) — определяемые в них типы. В свою очередь, тип (Туре) позволяет получить информацию обо всех его членах (вложенные типы, поля, конструкторы, методы, свойства и события). Пространства имен не входят в иерархию, так как они представляют собой синтаксические наборы типов. Если нужно перечислить все пространства имен, определенные в сборке, достаточно перечислить все типы в сборке и просмотреть их свойства Namespace.

Рис. 23.2. Типы, используемые приложениями для обхода объектной модели отражения


 

Тип позволяет также находить реализуемые им интерфейсы (как это сделать, я покажу позже). И из конструктора, метода, метода-аксессора свойства или метода создания/удаления сообщения можно вызвать метод GetParameters, чтобы получить массив объектов Panametenlnfo, которые информируют вас о типах параметров членов. Можно также запросить свойство ReturnParameter, чтобы получить объект Parameterlnfo с подробной информацией о возвращаемом значении члена. Чтобы получить набор параметров типа для обобщенных типов и методов, можно вызывать метод GetGenericArguments. Наконец, чтобы получить набор нестандартных атрибутов, примененных ко всем указанным сущностям, можно вызывать метод GetCustomAttributes.

Обращение к членам типов

Итак, вы знаете, как получить информацию о членах типа. Следующим шагом должно стать обращение к одному из этих членов. Термин «обращение» в данном случае зависит от разновидности члена. В табл. 23.2 указано, какие методы следует вызывать в каждом конкретном случае.

Таблица 23.2. Методы обращения к членам типов

Тип Описание
Field Info Метод GetValue получает значение поля, метод SetValue — задает его значение
Constructorlnfo Метод Invoke создает экземпляр типа и вызывает конструктор
Methodlnfo Метод Invoke вызывает метод типа
Propertylnfo Метод GetValue вызывает метод доступа get, метод SetValue — метод доступа set для свойства
Eventlnfo Метод Add Event Handler вызывает метод add, метод Remove Event Handler — метод remove для события


 


 

Тип Propertylnfo представляет информацию метаданных свойств (см. главу 10), поддерживая доступные только для чтения свойства CanRead, CanWrite и PnopentyType. Эти свойства показывают, можно ли читать и записывать свойство, а также его тип данных. У Pnopentylnf о также есть свойства GetMethod и SetMethod, возвращающие объекты Methodlnfo для методов чтения и записи значения свойства. Методы GetValue и SetValue типа Propertylnfo существуют для удобства, их внутренний код получает соответствующие объекты Methodlnfo и вызывает их. Для поддержки параметрических свойств (индексаторов С#) методы SetValue и GetValue предоставляют параметр index типа Object [ ].

Тип Eventlnfo представляет метаинформацию событий (см. главу 11). Он поддерживает доступное только для чтения свойство EventHandlerType, которое возвращает объект Туре для делегата события. У Eventlnfo также есть свойства AddMethod и RemoveMethod, которые возвращают объекты Methodlnfo для методов добавления и удаления делегатов. Чтобы добавить или удалить делегата, можно обратиться с вызовом к этим объектам Methodlnfo или же воспользоваться более удобными методами AddEventHandler и RemoveEventHandler класса Eventlnfo.

В приведенном далее приложении-примере демонстрируются разные способы применения отражения для доступа к членам типа. Класс SomeType представляет тип с различными членами: закрытым полем (m_someField), открытым конструктором (SomeType), получающим аргумент типа Int32 по ссылке, открытым методом (ToString), открытым свойством (SomeProp) и открытым событием (SomeEvent). При наличии определения типа SomeType я также могу предложить три разных метода использования отражения для доступа к членам SomeType. Каждый метод задействует отражение по-своему.