Метаданные в среде .Net

Автор: Андрей Мартынов
The RSDN Group

Источник: RSDN Magazine #2
Опубликовано: 14.03.2003
Версия текста: 1.1.1
Что такое метаданные и зачем они нужны?
Место метаданных в архитектуре .Net
Получение информации о типе
Получение экземпляра класса Type
Динамическая загрузка сборок
Динамическая загрузка типов
Исследование типа
Характеристики типа как целого
Члены класса
Утилита для исследования типов в сборке
Исследование объекта
Трассировка полей объекта
Динамическое создание объекта и вызов методов
Создание объекта по его типу
Динамический вызов методов
Использование интерфейсов
Позднее связывание
Динамическое приведение типов
Атрибуты
Декларативное программирование
Код, исполняющийся в design-time
Новые механизмы абстракции?
Динамическое создание типов
Динамический "Hello World!"
Динамическое разворачивание циклов
Динамическое программирование на C#.
Заключение
Что ещё почитать про метаданные в .Net

Исходные тексты примеров(~100 кб)

Что такое метаданные и зачем они нужны?

Для того, чтобы различные части программ могли взаимодействовать друг с другом, им необходимо обмениваться информацией о предоставляемых ими возможностях, и о том, каким образом эти возможности использовать. Например, если программа использует статическую библиотеку, к библиотеке обычно прилагается заголовочный файл, описывающий экспортируемые данные, процедуры и структуру типов. Другой пример – DLL. Чтобы использовать её в своей программе, вы, скорее всего, будете использовать соответствующие заголовочный файл и библиотеку импорта. Ещё один пример – COM-компонент. Описание его интерфейса обычно хранится в idl-файле или в виде специальных данных, в виде библиотеки типов. Все эти дополнительные файлы и данные, описывающие программные компоненты, называют метаданными.

Приставка мета подчеркивает что это – данные, описывающие другие данные. "Данные о данных".

В различных технологиях программирования используются метаданные разной степени подробности и разной степени универсальности. Но в технологии .Net метаданные играют совершенно особую роль. Это роль "универсального клея", на который возложены функции поставщика информации о типах как во время компиляции программы, так и во время её исполнения. Эта важная роль метaданных обусловлена следующими факторами:

1. Метаданные в .Net обязательны и универсальны.

Каждая программная единица в среде .Net, (сборка), помимо кода на языке MSIL обязательно содержит метаданные, описывающие как её в целом (манифест), так и каждый тип, содержащийся в ней, в отдельности (метаданные). На рисунке 1 показаны составные части однофайловой сборки.


Доступ к метаданным сборки является необходимым и достаточным условием доступа к содержащимся в ней типам.

2. Метаданные в .Net общедоступны.

Доступ к метаданным могут получить любые программные компоненты и любые инструменты программирования. Так, компилятор во время компиляции сборки использует информацию о зависимостях между сборками и проверяет соответствие используемых типов, извлекая нужную для этого информацию из метаданных. Инструменты программирования (в том числе среда программирования – IDE), извлекают информацию о типах и в удобном виде представляют структуру классов, а также предоставляют справочную информацию по ним (ObjectBrowser, IntelliSense). Набор утилит, обеспечивающих взаимодействие с COM-компонентами (Regasm, Tlbexp, Tlbimp), целиком опираются в своей работе на метаданные.

ПРИМЕЧАНИЕ

Да что там утилиты! Целые языки программирования работают исключительно благодаря метаданным. В среде .Net живут скриптовые, безтиповые языки (JScript и VBScript). Их работа основана на расшифровке и использовании метаданных во время исполнения программы. IDispatch отдыхает.

Сама сборка во время своего исполнения также имеет доступ к собственным метаданным. Отсюда название соответствующей технологии – Reflection .

ПРИМЕЧАНИЕ

РефлексИя (интроспекция), по Дж. Локку (1632-1704), - это "наблюдение, которому ум подвергает свою деятельность." Другими словами, рефлексия - это взгляд на самого себя, взгляд внутрь себя, способность к самоисследованию.

3. Метаданные в .Net исчерпывающи.

Метаданные содержат подробнейшую информацию о каждом типе: его имя, типы и имена его полей, описание свойств и методов со всеми их параметрами и возвращаемыми значениями. Здесь же хранится информация о доступности (видимости) всех членов класса и об их атрибутах. Метаданные хранят не только информацию о интерфейсе экспортируемых классов. Такие детали реализации, как структура защищённых полей, описания защищённых методов и других компонентов, также могут быть извлечены из метаданных.

Метаданные настолько подробны, что для создания и использования сборки не нужны никакие заголовочные файлы, IDL-файлы или библиотеки типов. Сборки могут использоваться без процесса регистрации. Разработчики .Net называют это компонентно-ориентированной технологией.

4. Метаданные в .Net расширяемы.

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

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

5. Метаданные в .Net конструируемы программно.

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

Имеются два способа динамического создания сборок, содержащих метаданные. Первый генерирует бинарный промежуточный код (MSIL), используя классы, лежащие в пространстве имен System.Reflection.Emit. Второй способ выдает результат в виде исходного кода на одном из поддерживаемых .Net Framework языков высокого уровня (C#, VB, ...), используя классы из пространства имен System.CodeDom.

В данной статье рассматриваются методы программирования в среде .Net с использованием метаданных: получение информации о типе во время исполнения (Reflection), динамическое создание объектов и вызов методов, решение задачи позднего связывания. Кратко обсуждаются вопросы, связанные с применением атрибутов. Приведены примеры генерации программного кода во время исполнения (Reflection.Emit).

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

1. RTTI (runtime type identification) в языке С++.

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

2. CRuntimeClass в библиотеке MFC.

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

3. Механизм Reflection в языке Java.

Глубоко встроенный в язык механизм. Механизм используется в реализации самих языковых конструкций. Имеются возможности исследования структуры типов во время исполнения, динамического создания объектов и вызова методов.

4. IDL – язык описания интерфейсов (Interface Definition Language)

5. Библиотеки типов (Type Libraries) в технологии программирования COM.

6. WSDL – язык описания Web-служб (Web Services Description Language). WSDL представляет собой стандарт XML-документов, предназначенный для описания возможностей Web-службы, включая формат принимаемых и передаваемых сообщений, имена и типы методов и функций, реализуемых службой, а также используемые протоколы обмена данными.

Место метаданных в архитектуре .Net

Технология .Net – это мощный, многоуровневый комплекс программных средств разработки, развёртывания и исполнения Web-сервисов и приложений (см. рис. 1). Самый нижний уровень комплекса состоит из двух частей: промежуточного языка (MS Intermediate Language, MSIL) и метаданных (metadata). На основе этих двух ключевых технологий построены основные системы CLR – системы загрузки и выполнения программ, система типов и управления памятью, сборка мусора (garbage collection), система проверки безопасности кода, обработка исключений и другие системы. Уровнем выше лежит мощная библиотека, содержащая базовые классы, классы-коллекции, классы ввода/вывода и другие классы, используемые в среде .Net. На эту библиотеку в свою очередь опираются более высокоуровневые технологии, такие как ASP.Net, XML Web Services, ADO.Net, Windows Forms Controls.

В данной статье мы совершим небольшую экскурсию по нижним этажам технологии .Net.

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

Каждый тип в среде .Net связан со специальным объектом (объектом-типом). Класс Type играет ключевую роль в системе работы с метаданными. Любая деятельность по извлечению и использованию информации о типе связана с применением этого класса. Экземпляр класса Type позволяет получить полную информацию о типе: информацию о его методах, свойствах, вложенных типах, информацию о сборке и модуле, содержащих данный тип, полное имя типа и многое другое.

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

Поэтому работа с типом начинается с получения соответствующего экземпляра класса Type.

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

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

1. Простейший случай – когда тип определён в вашей программе, или если при компиляции указана ссылка на сборку, содержащую данный тип. В этом случае экземпляр класса Type можно получить посредством операции typeof (все примеры в этой статье написаны на C#):

        using System;
namespace App1
{
  class Class1
  {
    staticvoid Main()
    {
      Type type = typeof(Class1);
      Console.WriteLine(type.FullName);
      Console.ReadLine();
    }
  }
}

То, что в примерах программ используется именно язык C#, не играет принципиальной роли. Дело в том, что возможности среды .Net по работе с метаданными доступны из любого языка программирования, удовлетворяющего CLS (common language specification). Поэтому вы всегда найдёте соответствующие конструкции в других языках. Например, операции typeof языка C# соответствуют операция __typeof из MC++ или оператор GetType из VB.NET.

2. Если есть экземпляр интересующего вас класса, то соответствующий ему экземпляр класса Type можно получить с помощью метода GetType. Этот метод получают в наследство от класса Object все классы в среде .Net.

Class1 app = new Class1();
Type type = app.GetType();

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

        using System;
using System.Reflection;
namespace App1
{
  class Class1
  {
    staticvoid Main()
    {
foreach (Type type in Assembly.GetExecutingAssembly().GetTypes())
        Console.WriteLine(type.Name);
      Console.ReadLine();
    }
  }
}

В этом примере для получения текущей сборки-приложения использован статический метод GetExetutingAssembly класса Assembly, затем с помощью метода GetTypes получен массив всех типов, содержащихся в текущей сборке.

Обращаю ваше внимание на то, что в этом примере было использовано пространство имён System.Reflection. Это главное вместилище классов и других типов, имеющих отношение к метаданным и рефлексии. Большинство типов, с которыми мы познакомимся ниже, определены в этом пространстве имён. Класс Type – исключение. Он настолько важен, что его описание помещено в пространство имён System.

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

Динамическая загрузка сборок

Приложение может загружать сборки из:

  1. Каталога приложения (private-сборки).
  2. Global Assembly Cach (GAC) (разделяемые сборки).
  3. Конкретного файла на диске.

Первые два способа идентифицируют сборку по имени.

Имя сборки содержит, помимо собственно имени, также ещё и версию сборки, информацию о локализации (Culture) и открытый ключ сборки (PublicKeyToken). Вот пример имени сборки:

System.Drawing, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a

Полное имя (т.е. содержащее все компоненты имени) однозначно идентифицирует сборку и по нему можно будет её загрузить.

Assembly asm = Assembly.Load("System.Drawing, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");

Имя сборки может быть как полным, так и частичным. Частичное имя позволяет задействовать специальный механизм версионности сборок. Метод LoadWithPartialName класса Assembly загружает сборку по частичной информации о ней.

Assembly asm = Assembly.LoadWithPartialName("System.Drawing");

В данном случае передано только имя сборки. В соответствии с политикой версий будет выбрана и загружена наиболее подходящая сборка. Этот способ более гибок, т.к. позволяет управлять загрузкой сборок, задавая или опуская различные параметры. Например, если вы укажете номер версии, то сборки другой версии (пусть даже более поздней!) использоваться не будут. А если явно задать культуру, то в будущем не нужно будет беспокоиться, что приложение случайно подключит новую версию сборки, не поддерживающую указанную культуру.

Динамическая загрузка сборок из каталога приложения не требует указания полного имени сборки, достаточно краткого имени.

Assembly asm = Assembly.Load("MyAssembly"); // загрузка private-сборки

Функции Assembly.Load и Assembly.LoadWithPartialName при выборе подходящей сборки первым делом просматривают каталог приложения, отдавая предпочтение private-сборкам.

ПРИМЕЧАНИЕ

Размещение сборок в каталоге приложения считается предпочтительным. Такой способ помогает предотвратить взаимное влияние приложений – "кошмар DLL".

Динамическая загрузка сборки по полному пути к файлу позволяет загрузить любую сборку в системе (не только private- или из GAC'а).

Assembly a = Assembly.LoadFrom(
  "D:\\WINNT\\Microsoft.NET\\Framework\\v1.0.3705\\System.Drawing.dll");

Однако этот способ недостаточно гибок и вряд ли стоит его широко применять.

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

Теперь, когда сборка загружена, можно извлечь из неё информацию о типе. Для этого необходимо использовать так называемое "квалифицированное имя типа" (Assembly Qualified Type Name). Квалифицированное имя типа состоит из двух частей: полного имени типа и полного или частичного имени сборки. Для получения описания метаданных некоторого типа его квалифицированное имя передаётся в статический метод GetType класса Type. В случае успеха этот метод возвращает экземпляр класса Type.

Assembly a = Assembly.LoadWithPartialName("System.Drawing");
string strAssemblyQualifiedTypeName = "System.Drawing.Rectangle, " 
                                      + a.FullName;
Type type  = Type.GetType(strAssemblyQualifiedTypeName);

В данном случае загрузка типа проведена в три этапа. Сначала загружена сборка, затем получено её полное имя, и только потом получен объект Type. Эти этапы можно объединить. Если вы знаете полное имя сборки, можно использовать его для составления квалифицированного имени типа, которое можно напрямую передать методу Type.GetType().

Type type  = Type.GetType("System.Drawing.Rectangle"
                      + ", System.Drawing"
                      + ", Version=1.0.3300.0"
                      + ", Culture=neutral"
                      + ", PublicKeyToken=b03f5f7f11d50a3a"
                      );

Исследование типа

Имея в руках объект Type, можно начинать исследовать структуру типа, который он описывает (перебирать поля, методы, события, свойства, вложенные типы...).

Характеристики типа как целого

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

IsAbstract Является ли тип абстрактным классом или интерфейсом.
IsClass Класс ли это? (т.е. не value type и не интерфейс)
IsSealed Может ли тип иметь наследников?
IsInterface Это интерфейс? (Заметьте, интерфейс - это не класс!)
IsCOMObject А уж не COM-объект ли скрывается под видом класса .Net?
IsValueType Это value type (тип-значение)?
IsEnum Это перечисление?
IsPrimitive Не является ли тип примитивным (т.е. одним из bool, byte, sbyte, short, ushort, int, uint, long, ulong, char, double, float)?
IsArray Является ли тип массивом. Тип элементов массива можно получить с помощью свойства ElementType.
IsPointer Является ли тип ссылкой? Тип указываемого объекта можно получить с помощью свойства ElementType.
IsByRef А уж не тип ли это параметра, передаваемого по ссылке? Если да, то тип передаваемого объекта можно получить с помощью свойства ElementType.
HasElementType HasElementType = IsArray || IsPointer || IsByRef. Т.е. тип является не самостоятельным типом, а лишь "производным от" ElementType.

Вот, например, как можно с помощью этих свойств получить описание семейства типа:

        string GetTypeDescription(Type type)
{
  return type.IsClass   ? "class"
    : type.IsInterface  ? "interface"
    : type.IsArray      ? "array"
    : type.IsEnum       ? "enum"
    : type.IsValueType  ? "struct"
    : "";
}

Члены класса

Для получения информации о членах класса имеется несколько групп методов (далее – семейств). Каждая группа обеспечивает получение информации об определённом типе членов класса. Например, информацию только о конструкторах класса, или только о его полях. Но есть одна группа методов, которые позволяют получать информацию сразу обо всех членах класса. Это семейство методов GetMembers.

        public MemberInfo[] GetMembers();
publicabstract MemberInfo[] GetMembers(BindingFlags);

В каждом семействе имеется не меньше двух методов, аналогичных приведенным выше. Один из этих методов возвращает массив, содержащий информацию об открытых (public) членах, а другой позволяет указать, информацию о каких членах класса следует возвращать. Например, если передать в качестве параметра BindingFlags.Public | BindingFlags.Instance, будут возвращены все открытые нестатические члены класса. Если вам нужна информация ещё и о статических членах, нужно добавить ещё один флаг BindingFlags.Static. А если вас интересуют не только открытые, но и закрытые члены, следует использовать флаг BindingFlags.NonPublic.

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

GetConstructors Получение информации о конструкторах класса. Возвращает массив элементов типа ConstructorInfo.
GetCustomAttributes Получение информации об атрибутах класса. Возвращает массив объектов-атрибутов.
GetEvents Получение информации о событиях, поддерживаемых классом. Возвращает массив элементов типа EventInfo.
GetFields Получение информации о полях класса. Возвращает массив элементов типа FieldInfo.
GetInterfaces Получение информации об интерфейсах, реализованных или унаследованных классом. Возвращает массив элементов типа Type.
GetMembers Получение информации о членах класса (включая свойства, методы, поля, события и т.д.) Возвращает массив элементов типа MemberInfo.
GetMethods Получение информации о методах класса. Возвращает массив элементов типа MethodInfo.
GetNestedTypes Получение информации о вложенных типах класса. Возвращает массив элементов типа Type.
GetProperties Получение информации о свойствах (properties) класса. Возвращает массив элементов типа PropertyInfo.
GetTypeArray Получение информации о типе элементов массива (если рассматриваемый тип является массивом). Возвращает соответствующий объект Type.

Поскольку и конструкторы, и поля, и события, и методы, и свойства - это всё члены класса, то совершенно логично, что соответствующие им типы метаданных, (ConstructorInfo, EventInfo, FieldInfo, MethodInfo, PropertyInfo) унаследованы от типа MemberInfo.

Давайте рассмотрим пример получения информации обо всех конструкторах класса.

Получение информации обо всех конструкторах класса
        public
        static
        string TraceCtorsOf(Type type)
{
  string  trace = "";
  ConstructorInfo[] arrCI = type.GetConstructors();
  foreach (ConstructorInfo ci in arrCI) 
  {
    trace += (ci.IsStatic   ? "static "     : "") 
           + (ci.IsPrivate  ? "private "    : "")
           + (ci.IsFamily   ? "protected "  : "")
           + (ci.IsAssembly ? "internal "   : "")
           + ci.Name; 
    ParameterInfo[] arrParamInfo = ci.GetParameters();
    trace +=  "(";
    for (int i = 0; i != arrParamInfo.Length; i++) 
    {
      ParameterInfo parInf = arrParamInfo[i];
      trace += (i != 0         ? ", "       : "") 
          + (parInf.IsIn       ? "in "      : "")
          + (parInf.IsOut      ? "out "     : "")
          + (parInf.IsOptional ? "optional ": "")
          + (parInf.IsLcid     ? "lcid "    : "")
          +  parInf.ParameterType.Name + " " 
          +  parInf.Name
          + ((parInf.DefaultValue != DBNull.Value) 
             ? (" = " + parInf.DefaultValue) : "")
          ;
    }
    trace += ");\r\n";
  }
  return trace;
}

В этом примере вы можете видеть, каким способом извлекается информация о параметрах методов. Каждая структура данных, отвечающая за методы-члены (EventInfo, MethodInfo, ConstructorInfo), имеет метод GetParameters(), возвращающий массив элементов типа ParameterInfo. Каждый элемент этого массива описывает отдельный параметр метода: его тип, имя, значение по умолчанию, направление передачи данных (in/out) и др.

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

Утилита для исследования типов в сборке

В состав .Net Framework SDK входит утилита для расшифровки метаданных – ildasm.exe. Хотя информация, предоставляемая этой утилитой, не всегда наглядна и очевидна (по причине сложности структуры метаданных), тем не менее, для проверки своего кода, работающего с метаданными, трудно найти лучшее подспорье.

Написать утилиту, во многом повторяющую функциональность ildasm.exe по исследованию типов, не составляет большого труда. Среди исходных текстов примеров, прилагающихся к этой статье, вы найдёте такую программу.


Гораздо большие возможности по исследованию и программированию типов предосталяет программа .Net Explorer, описанние которой вы найдёте на страницах этого журнала.

Исследование объекта

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

ПРИМЕЧАНИЕ

- Эка невидаль, - скажете вы мне. - Прочитать поля и свойства объекта! Что ж тут такого?

- Дело в том, что класс, экземпляр которого мы хотим создать и исследовать, не известен в момент написания программы. Более того, он не только не известен, он даже ещё и не написан! Его напишут после того, как программа будет скомпилирована!

Предположим, у вас имеется ссылка на объект неизвестного типа. В .Net это означает, что имеется ссылка на объект типа Object. Тип Object – это базовый тип CLR, и к нему можно привести любую ссылку (value-типы перед этим должны быть помещены в GC-хип, т.е. должна быть произведена операция boxing'а). У Object есть только базовый набор методов, через которые нельзя напрямую вызвать методы типа, приведенного к Object. Но с помощью Reflection можно динамически получить описание методов, полей или свойств объекта. А с помощью этого описания можно динамически к ним обращаться. Следующий пример считывает список полей и выводит их имена и значения:

      public
      static
      void TraceObjectFields(Object obj)
{
  foreach (FieldInfo mi in obj.GetType().GetFields()) 
    Console.WriteLine(mi.FieldType.Name 
         + " "  + mi.Name + "= " + mi.GetValue(obj);
}

А этот выводит список свойств объекта:

      public
      static
      void TraceObjectProperties(Object obj)
{
  foreach (PropertyInfo pi in obj.GetType().GetProperties()) 
    Console.WriteLine(pi.PropertyType.Name 
          + " " + pi.Name + " = " + pi.GetValue(obj,null);
}

Трассировка полей объекта

В процессе отладки иногда возникает необходимость запротоколировать состояние некоторых важных для работы программы классов. Написание кода протоколирования полей – не самая интересная работа, да и времени она может отнять много. К тому же может оказаться, что самое интересное поле класса всё-таки осталось не выведенным, а ситуация была трудновоспроизводимой... В таком случае можно порекомендовать вывести просто все (!) поля класса, тем более, что механизм reflection позволяет делать это одной строчкой кода.

ПРИМЕЧАНИЕ

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

- Не спорю. Действительно, ToString позволяет точно отрегулировать, как и какие данные класса выводить. Но мне кажется, что если это делается только для отладочных целей, поддержка методов ToString в сотнях (а может и в тысячах) классов – дело очень хлопотное. Гораздо проще однажды написать функцию, подобную приведённой ниже, и использовать её при необходимости.

Трассировка полей объекта
        static
        string ObjectFields(String pre, String name, Object obj)
{
  String  trace = "";
  Type  type  = obj.GetType();

  if (type.IsPrimitive)
  {
    trace += newLine + name + "= " + obj.ToString() 
          + ", " + type.FullName + AddIf(pre); 
  }
  elseif (type.IsArray)
  {
    Type typeElement  = type.GetElementType();        
    Array arr = (Array)obj;
    trace += newLine + name + ", Array of " + typeElement.FullName 
                + ", Rank= "  + arr.Rank 
                + ", Length= "  + arr.Length
                + AddIf(pre);

    int[] arrIdx = newint[arr.Rank];
    for (int dim = 0; dim < arr.Rank; dim++)
      arrIdx[dim] = 0;

    trace += Indent();
    for (int idx = arr.GetLowerBound(0); idx <= arr.GetUpperBound(0); idx++)
    {
      arrIdx[0] = idx;
      trace += ObjectFields("", name + "[" + idx + "]"
                , arr.GetValue(arrIdx));
    }
    trace += Unindent();
  }
  elseif (type.IsClass || type.IsValueType)
  {
    if (objects[obj] != null)
    {
      trace = newLine + name + "= " + (string)objects[obj]
          + ", " + type.FullName ;
    }
    else
    {
      objects[obj] = name;
      trace = newLine + name + ", " + type.FullName + AddIf(pre); 

      trace += Indent();
      foreach (FieldInfo fi in type.GetFields (
                      BindingFlags.NonPublic
                      | BindingFlags.Public
                      | BindingFlags.Instance
                      | BindingFlags.Static
                      )) 
      {
        if (! NeedTrace(fi))
          continue;

        string fieldName = name + "." + fi.Name;
        if (fi.IsStatic)
        {
          if (types[type.FullName+fi.Name] != null)
          {
            trace += newLine + fieldName + "= " 
                + (string)types[type.FullName + fi.Name]
                + ", " + fi.FieldType.FullName 
                + ", static";
            break;
          }
          else
          {
            types[type.FullName + fi.Name] = fieldName;

            object field = fi.GetValue(obj);
            if (field == null)
              trace += newLine + fieldName +  "= null"
                  + ", " + fi.FieldType.FullName 
                  + ", static";
            else
              trace += ObjectFields("static", fieldName
                        , field);
          }
        }
        else// Not static 
        {
          object field2 = fi.GetValue(obj);
          if (field2 == null)
            trace += newLine + fieldName +  "= null"
              + ", " + fi.FieldType.FullName;
          else
            trace += ObjectFields("", fieldName, field2);
        }
      }
      trace += Unindent();
    }
  }
  return trace;
}
...
static Hashtable types       = new Hashtable();
static Hashtable objects     = new Hashtable();

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

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

ПРИМЕЧАНИЕ

Еще одним бастионом безопасности кода в среде .Net является метод GetType() (тот самый, с рассмотрения которого начинался рассказ про метаданные). Дело в том, что метод GetType() не виртуальный, и поэтому злоумышленнику не удастся переопределить его, чтобы предоставить среде исполнения недостоверную информацию о типе и пробить тем самым брешь с системе защиты. GetType() реализуется самой средой исполнения, что гарантирует точную идентификацию типов и соблюдение правил безопасности.

Привилегии, разграничение доступа, система безопасности .Net – эти темы требуют отдельного, обстоятельного разговора, а пока воспользуемся тем, что в большинстве случаев метаданные доступны для чтения, и испытаем в деле процедуру протоколирования полей объекта.

Вот пример использования предложенного способа вывода полей класса.
        class Data1 
{ 
  publicstaticint k = 11;
  int[,]            b = {{1,0,0},{2,3,4},{5,0,0}};
  string            n = "reflection"; 
  public Class1     c;
}

class Class1
{
  public Data1 d = new Data1(), d2;
    
  staticvoid Main()
  {
      Class1 c = new Class1();
      c.d.c = c;
      Console.WriteLine(Trace2.ObjectFields("c", c));
      Console.ReadLine();
  }
}
Вот что должно вывестись на экран:
c, App1.Class1
{
  c.d2= null, App1.Data1
  c.d, App1.Data1
  {
    c.d.c= c, App1.Class1
    c.d.k= 11, System.Int32, static
    c.d.b, Array of System.Int32, Rank= 2, Length= 9
    {
      c.d.b[0]= 1, System.Int32
      c.d.b[1]= 2, System.Int32
      c.d.b[2]= 5, System.Int32
    }
    c.d.n, System.String
    {
      c.d.n.m_arrayLength= 11, System.Int32
      c.d.n.m_stringLength= 10, System.Int32
      c.d.n.m_firstChar= r, System.Char
      c.d.n.Empty, System.String, static
      {
        c.d.n.Empty.m_arrayLength= 1, System.Int32
        c.d.n.Empty.m_stringLength= 0, System.Int32
        c.d.n.Empty.m_firstChar=  , System.Char
        c.d.n.Empty.Empty= c.d2.n.Empty, System.String, static
      }
...  skiped    
    }
  }
}

Хочу обратить ваше внимание на имеющиеся здесь подводные камни. Посмотрим на поле типа string. Тип string имеет статическое поле Empty того же типа string. Поэтому, выводя поле Empty, вы снова обнаружите у него статическое поле Empty, выводя которое вы снова обнаружите статическое поле Empty... короче, получается бесконечная рекурсия. Выход простой – не выводить второй раз статические поля, а ссылаться на первый вывод этих полей.

Второй камушек связан с взаимными ссылками объектов друг на друга. В последнем примере класс Class1 имеет ссылку на класс Data1, а тот, в свою очередь, имеет ссылку на Class1. Если не предпринять специальных мер, то опять получится бесконечная рекурсия. Меры примерно такие же, как в предыдущем случае – запоминаем все выведенные объекты и не выводим их второй раз.

ПРИМЕЧАНИЕ

Кстати, заглянув в исходники Rotor'а, можно увидеть, что стандартные процедуры сериализации реализованы в .Net способом аналогичным тому, каким здесь реализовано протоколирование. Про сериализацию в .Net советую прочитать серию статей Джефри Рихтера в недавних номерах MSDN Magazine.

Динамическое создание объекта и вызов методов

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

Создание объекта по его типу

При создании объекта вызывается его конструктор. Если вас устраивает вариант создания объекта с вызовом конструктора без аргументов (конструктора по умолчанию), то задача решается очень просто. Например, вот так:

        class Class1
{
  int n = 5; 
    
  staticvoid Main()
  {
    string className = "App1.Class1";
    Type type = Type.GetType(className);
    Object data = Activator.CreateInstance(type);
    Console.WriteLine(Trace1.ObjectFields("data", data));
    Console.ReadLine();
  }
}

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

Примеры использования различных вариантов Activator.CreateInstance
        using System;
using System.Reflection;
using System.Diagnostics;

namespace ConsoleApplication2
{
  class Class1
  {
    publicstaticstring DotNetFrameworkDir()
    {
      Assembly testAssy = Assembly.Load("mscorlib.dll");
      return System.IO.Path.GetDirectoryName(testAssy.Location);
    }
    staticvoid Main()
    {
      // создаём объект по имени сборки и имени класса
      Object intObj = Activator.CreateInstance("mscorlib.dll"
        , "System.Int32");
      intObj = 5;
      Console.WriteLine(Trace2.ObjectFields("intObj", intObj));

      // создаём объект по файлу сборки и имени класса
      Object rgn = Activator.CreateInstanceFrom(DotNetFrameworkDir()
                              + "\\System.Drawing.dll"
                              , "System.Drawing.Region");
      Console.WriteLine(Trace2.ObjectFields("rgn", rgn));

      // Готовим параметры для вызова конструктора// Rectangle(Int32 x, Int32 y, Int32 width, Int32 height);object[] ctorArgs = { 10, 10, 100, 100 }; 

      // Грузим сборку
      Assembly assembly = Assembly.LoadWithPartialName("System.Drawing");
      Object rect = Activator.CreateInstance(assembly.FullName
          , "System.Drawing.Rectangle"
          , false// ignoreCase, учитывать регистр!
          , 0, null// по умолчанию
          , ctorArgs         // вот они - параметры!
          , null, null, null// всё остальное тоже по умолчанию
          );
      Console.WriteLine(Trace2.ObjectFields("rect", rect));

      Console.ReadLine();
    }
  }
}

ПРИМЕЧАНИЕ

Здесь применён простой способ получения пути к каталогу .Net Framework. Дело в том, что сборка mscorlib.dll используется в любой сборке, и потому всегда загружена. Остаётся только получить путь к папке, где лежит файл этой сборки, т.к. эта сборка всегда находится в каталоге .Net Framework.

Здесь приведён вариант создания объекта с использованием конcтруктора с параметрами. Составлен массив из параметров и передан в Activator.CreateInstance. Среди возможного множества конструкторов будет использован тот, который имеет в точности то же количество параметров и те же типы (и в том же порядке!), что и в переданном массиве параметров.

ПРИМЕЧАНИЕ

Наряду с классом Activator способностью создавать объекты по их типу обладают классы Assembly, AppDomain и некоторые другие. У них тоже есть методы CreateInstance, работа с которыми ведётся подобным образом.

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

На самом деле мы только что уже занимались динамическим вызовом методов. С конструкторами, по крайней мере, разобрались. Вызов остальных методов производится без помощи дополнительных классов типа Activator. Этим занимается сам класс Type.

Динамический вызов метода динамически созданного объекта:
        static
        void Main()
{
  // Получаем объект-тип для System.Drawing.Rectangle
  Assembly a = Assembly.LoadWithPartialName("System.Drawing");
  AssemblyName an = a.GetName();
  Type typeRect = Type.GetType("System.Drawing.Rectangle," 
                    + an.FullName, true);

  // Готовим параметры для вызова конструктора// Rectangle(Int32 x, Int32 y, Int32 width, Int32 height);object[] ctorArgs = { 10, 10, 100, 100 }; 
  Object rect = Activator.CreateInstance(typeRect, ctorArgs, null);

  // Выводим поля динамически созданного объекта
  Console.WriteLine(Trace1.ObjectFields("rect", rect));

  // Готовим параметры вызова метода Boolean Contains(Point pt);
  System.Drawing.Point point = new System.Drawing.Point(50, 50);
  object[] argContains = { point }; 
  object contain = typeRect.InvokeMember("Contains"
                          , BindingFlags.Public 
                          | BindingFlags.InvokeMethod
                          | BindingFlags.Instance
                          , null
                          , rect
                          , argContains
                          );  
  Console.WriteLine( contain.Equals(true) ? 
                     "contains" : "does not contain");
  Console.WriteLine(Trace1.ObjectFields("point", point));
  Console.ReadLine();
}

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

Использование интерфейсов

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

Использование динамически созданного объекта через интерфейс
        static
        void Main()
{
  // Получаем объект-тип для System.Drawing.SolidBrush
  Assembly a = Assembly.LoadWithPartialName("System.Drawing");
  AssemblyName an = a.GetName();
  Type typeBrush = Type.GetType("System.Drawing.SolidBrush,"
                                  + an.FullName, true);

  // Готовим параметры для вызова конструктора// SolidBrush(Color color);object[] ctorArgs2 = { System.Drawing.Color.Blue }; 
  Object brush = Activator.CreateInstance(typeBrush
                                      , ctorArgs2, null);

  // Выводим поля динамически созданного объекта
  Console.WriteLine(Trace1.ObjectFields("brush", brush));

  // Получаем ссылку на интерфейс
  ICloneable cloner = brush as ICloneable;
  if (cloner != null)
  {
    Object brushCloned = cloner.Clone();
    Console.WriteLine(Trace1.ObjectFields("brushCloned"
                                         , brushCloned));
  }
  Console.ReadLine();
}

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

Для получения ссылки на интерфейс ICloneable здесь применяется операция as языка C#. Эта операция возвращает ссылку на интерфейс, если он реализован объектом. Если объект не поддерживает данный интерфейс, будет возвращена пустая ссылка (null). Ниже будут подробнее рассмотрены другие способы динамического приведения типов.

Позднее связывание

Ну вот, теперь можно сказать, что мы умеем работать с типами, определяемыми в момент исполнения, почти столь же свободно, как и с типами, известными нам на момент компиляции. (Если я и преувеличил, то совсем чуть-чуть. 8-). Теперь мы вправе заслуженно насладиться плодами вновь освоенных технологий. Предлагаю вам в качестве небольшой передышки рассмотреть позднее связывание.

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

Не сомневаюсь, что многие из читающих эти строчки решали подобные задачи. Причем в разные времена эти задачи решались по-разному, начиная от LoadLibrary и GetProcAddress, и кончая COM-интерфейсами и категориями. Посмотрим, какое решение этой классической задачи предлагает технология .Net.

Интерфейс класса-обработчика будет иметь единственную функцию, определяющую, может ли данный класс обработать данный файл. Поместим интерфейс в отдельную сборку. На неё будут ссылаться конкретные классы-обработчики.

Интерфейс-обработчик.
      // compile with csc /t:library FileProcessor.cs 
      using System;
namespace FileProc
{
  interface IFileProcessor
  {
    bool IsSupport(string filePath);
  }
}

Теперь определим пару классов обработчиков. Один из них будет работать с текстовыми файлами, второй – с картинками в формате ВMP. При этом каждый будет реализовывать интерфейс-обработчик, и будет помещён в отдельную сборку, ссылающуюся на сборку с интерфейсом (см. рис.3).

Обработчик текстовых файлов.
      // compile with csc /t:library TextProcessor.cs /r:FileProcessor.cs
      using System;
using FileProc;
namespace TextProcessor
{
  publicclass TextProcessor : IFileProcessor
  {
    overridepublicbool IsSupport(string filePath)
    {
      return".txt" == IO.Path.GetExtension(filePath).ToLower();
    }
  }
}
Обработчик картинок.
      // compile with csc /t:library BmpProcessor.cs /r:FileProcessor.cs
      using System;
using FileProc;
namespace BmpProcessor
{
  publicclass BmpProcessor : IFileProcessor
  {
    overridepublicbool IsSupport(string filePath)
    {
      return".bmp" == IO.Path.GetExtension(filePath).ToLower();
    }
  }
}


В самом приложении, осуществляющем позднее связывание с классами-обработчиками, для вас уже почти ничего нового нет. Здесь просто загружаются все сборки из каталога приложения. Затем запрашиваются все типы из каждой сборки, и среди типов ищутся те, что унаследованы от абстрактного класса-обработчика. После того, как нужный тип найден, создаётся его экземпляр и вызывается метод IsSupport.

Приложение, использующее позднее связывание с классами-обработчиками.
      // compile with csc LateBind.cs /r:FileProcessor.cs
      using System;
using System.IO;
using System.Reflection;
using FileProc;

namespace LateBind
{
  class Class1
  {
    [STAThread]
    staticvoid Main(string[] args)
    {
      if(args.Length < 1)            
        return;
      string[] files = Directory.GetFiles(
        System.Environment.CurrentDirectory, "*.dll");
      foreach (string file in files)
      {
        Assembly assembly = Assembly.LoadFrom(file);
        Type[]   types    = assembly.GetTypes();
        foreach (Type type in types)
        {
        
          if (typeof(IFileProcessor).IsAssignableFrom(type) 
            && ! type.IsAbstract
            )
          {
            IFileProcessor processor 
              = (IFileProcessor)Activator.CreateInstance(type);
                    
if (processor.IsSupport(args[0]))
            {
              Console.WriteLine(
                  "Assembly {0} can process file {1}."
                , assembly.GetName().Name, args[0]);
              Console.ReadLine();
              return;
            }
          }
        }
      }
      Console.WriteLine("Can't process file {0}.", args[0]);
      Console.ReadLine();
    }
  }
}

Теперь, если появится необходимость обрабатывать файлы, например, формата VRML, не потребуется дорабатывать и перекомпилировать приложение. Достаточно будет разработать соответствующий класс-обработчик VRMLProcessor и поместить его в ту же папку, что и остальные классы-обработчики.

Динамическое приведение типов

Хочу обратить ваше внимание на то, как определялось, что тип, считанный из сборки, реализует необходимый интерфейс. В данном случае не было экземпляра объекта, к которому применялась бы операция as. Поэтому был использован метод IsAssignableFrom класса Type.

Говоря точнее, в большинстве случаев у нас есть возможность создать экземпляр объекта, а потом применить к нему as, но делать так не совсем рационально. Во-первых, создание объекта может быть недешёвым удовольствием, тем более что неизвестно, будем ли мы его использовать в дальнейшем, или он нам не подойдёт. Во-вторых, возможно, что объект всё равно не удастся создать, потому что тип может быть абстрактным классом или интерфейсом.

Класс Type имеет ещё один метод со схожей функциональностью – IsInstanceOfType. В отличие от IsAssignableFrom он принимает в качестве аргумента не объект Type, а экземпляр объекта. Метод IsInstanceOfType определяет, является ли тип объекта, переданного в аргументе, наследником типа, для которого этот метод вызван. Иначе можно это сформулировать вот так:

type.IsInstanceOfType(obj) = obj.GetType().IsAssignableFrom(type)

У Java-программистов первое время может возникнуть в голове путаница. Похожая по смыслу операция instanceof в Javа имеет такие же аргументы, но только в другом порядке. Она применяется к объекту и принимает в качестве аргумента имя типа. В Java операция instanceof совпадает по смыслу с операцией is в С#.

Если речь зашла о приведениях типов и о выяснении того, кто чей предок, то нельзя не коснуться операций is и as. Честно говоря, я начал немного путаться в этих функциях и операциях, и сделал себе такую табличку. Может, она и вам пригодится?

Возвращаемое значение Применяется к: Оператор или функция Тип аргумента Комментарий
bool Object is тип Оператор is позволяет определить, является ли объект (Object) наследником некоторого класса или интерфейса. Оператор также применим к массивам, и позволяет узнать, является ли объект массивом, совместимым с заданным типом.
Object Object as тип Оператор as пытается привести ссылку на объект или интерфейс (Object) к некоторому типу (возможно, интерфейсу). Если это невозможно (в смысле is), возвращается null.
bool Type IsInstanceOfType Object Позволяет определить, является ли некоторый экземпляр объекта (Object) экземпляром указанного типа (Type).
bool Type IsSubclassOf Type Позволяет определить, что тип является наследником типа, переданного в качестве аргумента, и не является абстрактным. Если типы одинаковые, возвращается false.
bool Type IsAssignableFrom Type Позволяет определить, что в переменную данного типа можно установить значение, тип которого указан в качестве параметра метода IsAssignableFrom. Также говорит о возможности приведения типов.

Атрибуты

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

ПРИМЕЧАНИЕ

Работа с атрибутами подробно освещена в статье Андрея Алифанова [5]. В ней вы найдёте подробные примеры работы, а также исходные тексты демонстрационного приложения. Наличие этой статьи на RSDN.ru сильно облегчает мою задачу и позволяет не останавливаться подробно на технической стороне дела. Я позволю себе подчеркнуть только некоторые принципиальные моменты и привести небольшой пример использования атрибутов (эту статью можно найти на нашем компакт-диске).

Декларативное программирование

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

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

[ AttributeUsage(AttributeTargets.Field) ]
publicclass NotTrace : Attribute
{
}

Чтобы упростить использование этого атрибута, напишем простую вспомогательную функцию:

        static
        bool NeedTrace(FieldInfo fi)
{
return ! Attribute.IsDefined(fi, typeof(NotTrace));
}

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

        foreach (FieldInfo fi in type.GetFields(...))
{
  if (! NeedTrace(fi))
    continue;
  ... 
}

Заметьте, что теперь управление выводом полей производится не изменением кода функции протоколирования, а изменением описаний полей – заданием их атрибутов. Другими словами, судьба объекта задаётся при его декларации. Отсюда название данного метода программирования – декларативное программирование.

ПРИМЕЧАНИЕ

Советую заглянуть в статью Игоря Ткачёва "Память больше не ресурс", опубликованную в RSDN Magazine #1, где приводится изящный пример использования атрибутов. В этой статье атрибуты используются для управления автоматическим вызовом Dispose.

Код, исполняющийся в design-time

На примере атрибутов мы впервые (я, по крайней мере, впервые) столкнулись с тем, что часть кода, который пишет программист, выполняется в runtime (это как обычно), а часть кода выполняется в момент разработки (design time)! Не просто используются описания типов (декларации), а именно код! Ведь чтобы получить информацию об атрибутах, надо создать экземпляры объектов-атрибутов, а значит, в этот момент отрабатывают конструкторы атрибутов.

Вот как пишет об использовании атрибутов компилятором Э. Гуннерсон [2]:

Когда компилятор находит атрибут X, установленный для класса, он сначала ищет класс, производный от Attribute, с именем X. Не обнаружив такого класса, он начинает искать класс XAttribute. На этот раз поиск заканчивается успешно. После этого компилятор проверяет, можно ли использовать данный атрибут для классов (AttrubuteUsage). Затем начинается поиск конструктора, который бы соответствовал параметрам, указанным при установке атрибута, если такой конструктор найден, компилятор создаёт экземпляр объекта вызовом конструктора с заданными параметрами. При передаче именованных параметров компилятор сопоставляет имя параметра с именем переменной класса или свойства, после чего присваивает переменной или свойству указанное значение. Во всяком случае, так должно происходить на логическом уровне. <...>

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

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

От себя же добавлю, что такая схема работы компилятора принципиально ничего не меняет. Дело в том, что другие программные инструменты могут создавать объекты атрибутов "на самом деле", да и сам компилятор в следующих версиях .Net Framework вполне может начать работать по алгоритму, близкому к тому, по которому он работает на логическом уровне. Потому нам следует думать об атрибутах как об объектах, которые могут работать в design time.

Новые механизмы абстракции?

Теперь, когда мы завершили краткое рассмотрение методов исследования типов (методов рефлексии), у нас есть повод задуматься над вопросом: "Какое новое качество могут принести методы рефлексии в наши программы. Дают ли они в наши руки принципиально новые возможности? Думаю этот вопрос имеет положительный ответ.

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

Формулируя алгоритм с помощью абстрактных типов данных (шаблонов), вы предполагаете наличие определённых свойств у типов, которые будут «подставлены» в шаблон (параметров шаблона). Конкретная реализация алгоритма становится ясной тогда, когда становятся известны типы-параметры шаблона, т.е. во время компиляции программы, использующей шаблон. («конкретизация при компиляции»)

Формулируя алгоритм с помощью абстрактных (виртуальных) методов класса, вы откладываете окончательную реализацию алгоритма на время исполнения программы. Именно тогда, во время исполнения, решается то, какие реальные методы будут участвовать в работе алгоритма. Это зависит от того с какими конкретными объектами придётся работать алгоритму. Можно сказать, что виртуальные методы конкретизируются в runtime. («конкретизация времени исполнения»).

Но заметьте, виртуальными (абстрактными) до сих пор могли быть только методы. А если вам нужно применить тот же самый механизм конкретизации времени исполнения, но по отношению к полям объекта? А если вам необходимы абстрактные структуры данных, конкретизируемые не при компиляции, а во время исполнения? Всё это позволяет сделать reflection. Короче, reflection – это виртуализация всех элементов программы.

Теперь нам позволительно формулировать алгоритмы подобные таким:

«Найди в этой структуре поле обозначающее цену товара и верни её». Перебираем все поля класса, в поисках поля типа Currency. Заметьте вам не надо знать имя поля. Если вы знаете что поле такого типа есть и оно одно – этого достаточно!

«Свяжи поля этой структуры с аргументами той хранимой процедуры на основе типов и имён полей и параметров». В последнем номере русского MSDN отличная статья про подобного рода методы.

«Выведи на консоль поля c атрибутом «видимый» (этим мы только что занимались).

«Предложи пользователю выбрать класс, заполнить параметры его конструктора класса и создай экземпляр этого класса». Эту задачу и многие подобные ей решает программа .Net Explorer описанная в статье этом номере нашего журнала.

У меня складывается впечатление, что наконец то мы с вами стали настоящими хозяевами своих программ! Наконец то нам стало доступно то, что раньше мог делать только «его величество компилятор». Уверен, что эти новые возможности породят совершенно новые классы алгоритмов, новые подходы к программированию, что в свою очередь принесёт новое качество в наши программы. Поживём – увидим.

А пока рассмотрим ещё один приём программирования в среде .Net, тесно связанный с метаданными, и сулящий не менее революционные изменения в методах программирования.

Динамическое создание типов

Да. Заголовок этой главки написан правильно. Опечатки нет. Именно создание типа в момент исполнения программы. То есть в каком-то смысле программы начинают программировать себя сами...

Динамический "Hello World!"

Значит, начинаем процесс обучения программ программированию? Тогда не будем отступать от традиций и начнём обучение с классической программы "Hello World!".

Динамический вариант "Hello World!"
        using System;
using System.Reflection;
using System.Reflection.Emit;

namespace DynHelloWorld
{
  class Programmer
  {
    staticpublic Type WriteCode()
    {
      AssemblyName assemblyName = new AssemblyName();
      assemblyName.Name = "HelloWorldAssembly";

      AssemblyBuilder assemblyBuilder = 
        AppDomain.CurrentDomain.DefineDynamicAssembly(
              assemblyName, AssemblyBuilderAccess.Run);

      ModuleBuilder moduleBuilder = 
        assemblyBuilder.DefineDynamicModule("HelloWorldModule");

      TypeBuilder typeBuilder = 
        moduleBuilder.DefineType("HelloWorldClass"
                                , TypeAttributes.Public);

      MethodBuilder methodBuilder = 
        typeBuilder.DefineMethod("HelloWorld"
                        , MethodAttributes.Public
                        , null, null);

      ILGenerator il = methodBuilder.GetILGenerator();

      // Генерируем код.il.EmitWriteLine("Hello World!");
      il.Emit(OpCodes.Ret);
return typeBuilder.CreateType();
    }
  }

  class Class1
  {
    staticvoid Main()
    {
      Type       typeCode   = Programmer.WriteCode();
      object     objCode    = Activator.CreateInstance(typeCode);
      MethodInfo methodInfo = typeCode.GetMethod("HelloWorld");

      methodInfo.Invoke(objCode, null);

      Console.ReadLine();
    }
  }
}
ПРИМЕЧАНИЕ

Помните, в программистском юморе была история о создании HelloWorld программистами разного уровня подготовки? Самый квалифицированный программист ваял все это на COM, и сумел занять около 30 строк кода, при этом чувствовалась явная натянутость. Салага! Вот как надо писать HelloWorld'ы. На новом технологическом витке HelloWorld в 53 строки вполне строен и лаконичен. - прим. ред.

Класс-программист (класса Programmer) пишет код методом WriteCode(). Код – это метод класса HelloWorldClass, который содержится в модуле HelloWorldModule, который принадлежит сборке HelloWorldAssembly.

Класс-программист создаёт эти объекты с помощью набора соответствующих объектов-Buider'oв, прописанных в пространстве имён System.Reflection.Emit, попутно задавая атрибуты создаваемых объектов. В данном случае и тип, и метод создаются как открытые (об этом говорят флаги TypeAttributes.Public и MethodAttributes.Public).

Самое интересное, конечно – это непосредственное генерирование кода. Он в данном случае состоит всего из двух команд языка IL: вывод строки на консоль и возврат из процедуры.

Забавно? Только и всего?

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

Но для этого надо обеспечить, чтобы сборка генерировалась при первой необходимости. О потребности в сборке можно узнать, перехватив событие AssemblyResolve класса AppDomain. Это событие генерируется при любой неудачной попытке загрузить в домен какую-либо сборку. А так как нашей сборки ещё нет (она ещё не сгенерирована), то любая попытка её загрузить будет неудачной.

Среди прилагающихся к статье примеров есть соответствующий этому случаю.

Динамическое разворачивание циклов

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

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

        int result = 0;
for (int i = 1; i <= valMax; i++)
    result += i;
ПРЕДУПРЕЖДЕНИЕ

Нас интересует именно непосредственное суммирование, т.е. мы не хотим пользоваться общеизвестной формулой нахождения суммы арифметической прогрессии. Представьте себе, что наша задача как раз и состоит в проверке правильности этой формулы! 8-)

Для решения этой задачи используется простейший цикл с верхней границей valMax. Величина valMax является параметром этого алгоритма и может меняться от одного запуска алгоритма к другому.

То, что valMax является величиной переменной, играет важную роль для оптимизации этого цикла. Дело в том, что если бы в задаче требовалось провести вычисления только для одного значения valMax, можно было бы обойтись без цикла. Например, для valMax = 20 код выглядел бы вот так.

        int result = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10
     + 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20;

Какой смысл в таком преобразовании кода? – спросите вы. Думаю, что убедительнее всего будет вам самим произвести замер скорости выполнения двух вариантов суммирования. Вы убедитесь, что вариант с циклом проигрывает по скорости второму варианту в несколько раз! Надо сказать, что разработчики компиляторов прекрасно осведомлены в этом вопросе и давно используют приём разворачивания циклов при трансляции кода циклов.

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

Постойте, но ведь технология Reflection.Emit позволяет генерировать код во время исполнения программы. Это значит, что мы можем отложить генерирование кода цикла до того момента, когда количество необходимых повторений станет известным. В этот момент мы можем "на лету" сгенерировать код развёрнутого цикла и получить огромный выигрыш в скорости!

Динамическое разворачивание цикла
        using System;
using System.Reflection;
using System.Reflection.Emit;

namespace DynUnloop
{
  // Суммирование в циклеclass SumLooping
  {
    publicint Summ(int valMax)
    {
      int result = 0;
      for (int i = 0; i <= valMax; i++)
        result += i;
      return result;
    }
  }

  // Плоское суммированиеclass SumFlat
  {
    interface ISumCode
    {
      int ComputeSumm(int valMax);
    }
    void WriteCode(int valMax)
    {
      AssemblyName assemblyName = new AssemblyName();
      assemblyName.Name = "SumFlatAssembly";

      AssemblyBuilder assemblyBuilder = 
        AppDomain.CurrentDomain.DefineDynamicAssembly(
        assemblyName, AssemblyBuilderAccess.Run);

      ModuleBuilder moduleBuilder = 
        assemblyBuilder.DefineDynamicModule("SumFlatModule");

      TypeBuilder typeBuilder = 
        moduleBuilder.DefineType("SumFlatClass"
                    , TypeAttributes.Public);

      typeBuilder.AddInterfaceImplementation(typeof(ISumCode));

      /// Задаём возвращаемое зачение и параметр
      Type[] paramTypes = { typeof(int) };
      Type   returnType = typeof(int);

      MethodBuilder methodBuilder = 
        typeBuilder.DefineMethod("ComputeSumm"
          , MethodAttributes.Public 
          | MethodAttributes.Virtual
          , returnType, paramTypes);

      ILGenerator il = methodBuilder.GetILGenerator();

      // Генерируем плоский код.      il.Emit(OpCodes.Ldc_I4, 0);
      for (int i = 1; i <= valMax; i++)
      {
        il.Emit(OpCodes.Ldc_I4, i);
        il.Emit(OpCodes.Add);
      }
      il.Emit(OpCodes.Ret);

// Перекрываем метод ComputeSumm и создаём тип SumFlatClass.
      MethodInfo methodInfo = typeof(ISumCode).GetMethod("ComputeSumm");
      typeBuilder.DefineMethodOverride(methodBuilder, methodInfo);
      typeBuilder.CreateType();

      /// Код готов, создаём объект и берем его интерфейс.
      code = (ISumCode)assemblyBuilder.CreateInstance("SumFlatClass");
    }

    publicint Summ(int val)
    {
      if (this.code == null)
        WriteCode(val);
      returnthis.code.ComputeSumm(val);
    }

    ISumCode code;
  }

  class Test
  {
    staticvoid Main()
    {
      constint valMax = 3000;
      constint countIterations = 200000;

      ////////////////////////////////////////////
      
      SumLooping sumLoop = new SumLooping();
      DateTime start = DateTime.Now;
      
      int sum = 0;
      for (int it = 0; it < countIterations; it++)
        sum = sumLoop.Summ(valMax);

      TimeSpan span = DateTime.Now - start;
      Console.WriteLine("Sum Looping. Sum = {0}, Elapsed msec= {1}"
          , sum, span.TotalMilliseconds);

      ///////////////////////////////////////////
      
      SumFlat sumFlat = new SumFlat();
      DateTime start2 = DateTime.Now;

      int sum2 = 0;
      for (int it = 0; it < countIterations; it++)
        sum2 = sumFlat.Summ(valMax);;

      TimeSpan span2 = DateTime.Now - start2;
      Console.WriteLine("Sum Flat. Sum = {0}, Elapsed msec= {1}"
        , sum2, span2.TotalMilliseconds);

      Console.ReadLine();
    }
  }
}

Вот такой получается результат:

Sum Looping. Sum = 4501500, Elapsed msec= 4967,1424
Sum Flat. Sum = 4501500, Elapsed msec= 731,0512

Выигрыш от разворачивания цикла – в 4-7 раз.

ПРИМЕЧАНИЕ

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

Результат, я думаю, получился убедительным, но вот способ получения его результата, думаю, привёл некоторых в смущение. Всё-таки генерирование Op-кодов MSIL, да ещё динамическое, при помощи методов Reflection.Emit – это занятие непростое и трудоёмкое. А нельзя ли применить обычные языки среды .Net для динамического программирования? C#, например? Можно.

Динамическое программирование на C#.

Рассмотрим ещё одну задачу. Имеется два массива целых чисел – массив А и массив Б. Наша задача подсчитать количество элементов массива Б, совпадающих с каким либо элементом массива А.

Простейшее решение

        public
        int Search(int[] a, int[] b)
{
  int count = 0;
  foreach (int i in b)
  { 
foreach (int j in a)
    { 
      if (i == j)
      { 
        count++; 
        break;
      } 
    } 
  }
  return countt;
}

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

        switch (i)
{
  case a[0]:
  case a[1]: 
  case a[b.Length - 1]:
    returntrue;
}
returnfalse;

Разработчики компиляторов достигли больших успехов в оптимизации оператора switch. Он выполняется много быстрее соответствующего цикла. Этим мы и хотим воспользоваться. Мешает одно – в операторе case могут фигурировать только константные выражения, а значения элементов массива нам при компиляции не известны.

Но известны в во время исполнения! Поэтому мы принимаем решение написать код этого switch’а во время исполнения. Надеюсь, что теперь вас это уже совсем не удивляет. Остаётся только вопрос «как?». Ответ - switch будем писать прямо на C# ! Готовы? Тогда вперёд!

Пример динамического программирования на C#.
        using System;
using System.Reflection;
using System.Text;
using System.CodeDom.Compiler;
using Microsoft.CSharp;

namespace DynUnloop
{
  // Проверка в циклеclass SearchLooping
  {
    publicint Search(int[] a, int[] b)
    {
      int count = 0;
      foreach (int i in b)
      { foreach (int j in a)
        { if (i == j)
          { 
            count++; 
            break;
      } } }
      return count;
    }
  }

  // Проверка cгенерированным switch'омpublicinterface IChecker
  {
    bool Check(int valMax);
  }
  class SearchFlat
  {
    IChecker WriteCode(int[] a)
    {
      StringBuilder code = new StringBuilder();
      code.Append("\n namespace DynUnloop" );
      code.Append("\n { class Checker : IChecker" );
      code.Append("\n   { bool IChecker.Check(int n)");
      code.Append("\n     { switch (n)" );
      code.Append("\n       {" );
      foreach (int j in a)
        code.Append("\n       case "+ j +":" );
      code.Append("\n           return true;" );
      code.Append("\n       }" );
      code.Append("\n       return false;" );
      code.Append("\n } } }\n" );
      //Console.Write(code.ToString()); // проверяем сгенерированный код

      CSharpCodeProvider cs   = new CSharpCodeProvider();
      ICodeCompiler      csc  = cs.CreateCompiler();
      CompilerParameters pars = new CompilerParameters();

      pars.ReferencedAssemblies.Add( 
        Assembly.GetExecutingAssembly().Location); 

      CompilerResults result = csc.CompileAssemblyFromSource(
        pars, code.ToString()); // компилируем!if (result.Errors.Count != 0)
      {
        foreach(CompilerError err in result.Errors)
          Console.WriteLine(err.ToString());
        returnnull;
      }
      Assembly asm = Assembly.LoadFrom(result.PathToAssembly);
      return (IChecker)asm.CreateInstance("DynUnloop.Checker");
    }

    publicint Search(int[] arr1, int[] arr2)
    {
      if (this.code == null)
        this.code = WriteCode(arr1);

      int result = 0;
      foreach (int i in arr2)
      {
        if (this.code.Check(i)) // используем сгенерированный код
          result++;
      }
      return result;
    }

    IChecker code = null;
  }

  class Test
  {
    [STAThread]
    staticvoid Main(string[] args)
    {
      int[] a = { // Подсчитываем вхождения этих чисел в масссив arr2.
74, 97, 93, 86, 2, 78, 48, 14, 21, 58, 60, 5, 39, 4, 66, 9
, 31, 15, 69, 27, 37, 46, 62, 61, 81, 17, 88, 19, 44, 8 
                };
      int[] b = { // В этом массиве ищем числа из, содержащиеся в arr1.
  98, 53, 79, 47,  0, 39, 28, 18, 39, 49, 56, 17, 33, 19, 72, 13
, 28, 48, 21, 80, 10,  3, 67, 76, 83,  6, 40, 58, 23, 74, 81, 88
, 13, 48, 59, 83, 47,  1, 38, 63, 70, 21, 23, 30, 86, 71, 15, 25
, 32, 73, 23, 55, 52, 19, 90, 95, 84,  2, 63, 93, 98, 69, 93, 64
, 66, 66,  3, 84, 58, 88, 64, 26,  9, 56,  9, 88, 78, 37, 88, 11
, 89, 14, 26, 49, 50, 26, 36, 93, 56, 63, 97, 44, 37, 44, 64,  1
, 26, 58, 62, 19, 68, 30, 66, 42,  9, 96, 45, 94,  9,  2, 17, 46
, 12, 51,  3, 83, 43, 44, 14, 40, 30,  9, 27, 94, 90, 19, 87, 64
, 91,  8, 61, 20, 74, 69, 42, 59, 47, 82, 40, 52, 80, 41, 83, 54
, 45, 50, 31, 85, 41, 80, 56, 80, 44, 22, 88, 58,  3, 70, 51, 88
,  8, 80,  2,  1, 39, 96, 71, 42,  8, 43, 35, 59,  4, 60, 59, 88
, 25, 72, 48, 39, 86,  1, 23, 11, 50, 79, 74, 52, 79, 83, 56, 75
, 31, 50, 43,  0, 38, 82, 14, 48, 78, 88, 77, 97, 44, 96, 76, 83
, 61,  0, 32, 30, 22, 12,  1,  7, 56, 90, 49, 58, 21, 18, 62, 23
, 85, 58, 28, 52, 16, 58, 49, 42, 57, 98, 59, 97, 23, 25, 65, 53
,  3, 90, 89, 79, 50, 25, 53, 18, 49, 36, 42, 47, 33, 54, 27, 59 
                };

      constint countIterations = 200000;

      ////////////////////////////////////////////

      SearchLooping searchLoop = new SearchLooping();
      DateTime start = DateTime.Now;

      int count = 0;
      for (int it = 0; it < countIterations; it++)
        count = searchLoop.Search(a, b);

      TimeSpan span = DateTime.Now - start;
      Console.WriteLine(
        "Search with looping. Count = {0}, Elapsed msec= {1}"
        , count, span.TotalMilliseconds);

      ///////////////////////////////////////////

      SearchFlat searchFlat = new SearchFlat();
      DateTime start2 = DateTime.Now;

      int count2 = 0;
      for (int it = 0; it < countIterations; it++)
        count2 = searchFlat.Search(a, b);

      TimeSpan span2 = DateTime.Now - start2;
      Console.WriteLine(
        "Search with switch. Count = {0}, Elapsed msec= {1}"
        , count2, span2.TotalMilliseconds);

      Console.ReadLine();
    }
  }
}

Вот результат, говорящий сам за себя.

Search with looping. Count = 84, Elapsed msec= 22482,328
Search with switch. Count = 84, Elapsed msec= 5487,8912

СОВЕТ

Пытливый читатель конечно заметит, что динамически созданный код соревновался с не совсем оптимально написанным поиском по массиву. Было бы гораздо оптимальнее предварительно отсортировать массив, с тем чтобы потом применить двоичный поиск. А вот в этом соревновании вряд ли будет выявлен явный лидер, т.к. сам реализация switch’а сама использует двоичный поиск. Ну что ж. Тогда можно считать, что предложенный способ это всего лишь автоматизация сортировки и двоичного поиска, переложенная на плечи компилятора. 8-)

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

Это задачи с гибким (многовариантным) нижним уровнем. (Название я придумал сам, так что наверняка есть более правильное.) Что я имею в виду под словами "гибкий нижний уровень"?

Примерно следующее. Представьте себе, что вы разрабатываете библиотеку для обработки некоторых данных. При этом все ваши алгоритмы оперируют данными с помощью некоторых унифицированных методов доступа. Эти методы доступа скрывают от ваших алгоритмов всё разнообразие типов данных и способов доступа к ним, скрывают их разнообразные параметры и настройки. Методы доступа универсальны (гибки), а, значит, вряд ли обладают достаточной эффективностью (универсальность и эффективность редко сочетаются). Вы могли бы реализовать частные методы доступа, но разнообразие типов и параметров данных так велико, что задача становится невыполнимой. Из-за этого вам приходится мириться с недостаточной эффективностью кода.

Тут на помощь приходит динамическое программирование. Частные методы доступа можно сгенерировать "на лету", в тот момент, когда типы и параметры данных вам уже известны, что позволит создать максимально эффективный код. В этом случае нет нужды обеспечивать универсальность, ведь сгенерированный код будет использован только для того типа данных, с которым вы работаете сейчас.

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

Примером может служить реализация класса RegExp в демонстрационной версии CLR – Rotor. Класс RegExp позволяет производить поиск и замену в тексте на основе регулярных выражений. В данном случае регулярное выражение играет роль того самого гибкого метода доступа (фильтра). Динамическое генерирование метода проверки соответствия регулярному выражению, позволяет в несколько раз повысить скорость поиска и замены текста. Подробности реализации класса RegExp вы можете изучить по исходникам Rotor'а [7].

Заключение

Мне же на этом пора заканчивать. Надеюсь, мне удалось вызвать у вас интерес к обсуждаемой теме и помочь сделать первые шаги в столь увлекательной и перспективной области, как программирование с использованием метаданных. Удачи!

Что ещё почитать про метаданные в .Net

  1. Том Арчер. Основы C#. Русская редакция. 2001.
    Глава 8. Атрибуты.
    Глава 16. Получение метаданных с помощью отражения..
  2. Э. Гунерсон. Введение в C#. Питер. 2001.
    Глава 31. Подробности о рефлексии.
  3. O'Reilly Book Excerpts: Programming C#
    Chapter 18: Attributes and Reflection (by Jesse Liberty)
  4. Владислав Чистяков. CLR
    Цитата: "Если модель программирования CLR так похожа на модель программирования COM, зачем она вообще нужна? Ответ в эволюции информации о типах. Главное достоинство CLR - всепроникающая, расширяемая, качественная информация о типах."
    Сайт издательства Оптим.Ру богат и другими материалами по .Net на русском языке.
    http://www.optim.su
  5. Андрей Алифанов. Использование атрибутов в среде .Net
    Подробно и всестороннее рассмотрение работы с атрибутами, сопровождающееся примерами кода и исходными тестами демонстрационного приложения.
    http://www.rsdn.ru
  6. Олег Степанов. Reflection в .NET
    Помимо этой статьи на сайте GotDotNet вы найдёте большое количество информации по .Net на русском языке.
    http://www.gotdotnet.ru
  7. The Shared Source CLI Beta.
    Исходные тексты демонстрационной версии CLR. Предназначены "для изучения и проведения исследований". Компилируются для двух платформ – WindowsXP и FreeBSD. В прилагающейся документации можно обратиться к документам: "Metadata definition and semantics", "Assembly Manifest Specification" и "Metadata Unmanaged API".

Эта статья опубликована в журнале RSDN Magazine #2. Информацию о журнале можно найти здесь