ПРОГРАММИРОВАНИЕ    НА    V I S U A L   C + +
РАССЫЛКА САЙТА       
RSDN.RU  

    Выпуск No. 83 от 16 марта 2003 г.
   
Подписчиков: 20107 

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

Здравствуйте, дорогие подписчики!


 CТАТЬЯ

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

Автор: Андрей Мартынов
Источник: RSDN Magazine #2
Исходные тексты примеров(~100 кб)
Что такое метаданные и зачем они нужны?

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

ПРИМЕЧАНИЕ

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

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

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

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

Рисунок 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
  {
    static void 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
  {
    static void 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();
public abstract 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 по исследованию типов, не составляет большого труда. Среди исходных текстов примеров, прилагающихся к этой статье, вы найдёте такую программу.

Рисунок 2

Гораздо большие возможности по исследованию и программированию типов предосталяет программа .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); 
  }
  else if (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 = new int[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();
  }
  else if (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 
{ 
  public static int 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;
    
  static void 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.

Окончание статьи - в следующем выпуске (или на RSDN)


Ведущий рассылки: Алекс Jenter   jenter@rsdn.ru
Публикуемые в рассылке материалы принадлежат сайту RSDN.

| Предыдущие выпуски     | Статистика рассылки