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


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

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



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

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

Источник: RSDN Magazine #2

Опубликовано: 14.03.2003
Исправлено: 05.01.2007
Версия текста: 1.1.1

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

function ToggleCode(id) { el=document.getElementById(id); img=document.getElementById("img"+id); if(el.style.display=="none") { img.src="/images/ls2.gif"; el.style.display=""; } else { img.src="/images/ls1.gif"; el.style.display="none"; } return false; } Исходные тексты примеров(~100 кб)

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

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

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

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

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

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

 

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

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

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

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

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

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

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

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

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

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

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

Теперь, когда сборка загружена, можно извлечь из неё информацию о типе. Для этого необходимо использовать так называемое "квалифицированное имя типа" (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, можно начинать исследовать структуру типа, который он описывает (перебирать поля, методы, события, свойства, вложенные типы...).

Члены класса

Для получения информации о членах класса имеется несколько групп методов (далее – семейств). Каждая группа обеспечивает получение информации об определённом типе членов класса. Например, информацию только о конструкторах класса, или только о его полях. Но есть одна группа методов, которые позволяют получать информацию сразу обо всех членах класса. Это семейство методов 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 это означает, что имеется ссылка на объект типа 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.

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

На самом деле мы только что уже занимались динамическим вызовом методов. С конструкторами, по крайней мере, разобрались. Вызов остальных методов производится без помощи дополнительных классов типа 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.csusing System;using FileProc;namespace TextProcessor{ public class TextProcessor: IFileProcessor { override public bool IsSupport(string filePath) { return ".txt" == IO.Path.GetExtension(filePath).ToLower(); } }}

Обработчик картинок.

// compile with csc /t:library BmpProcessor.cs /r:FileProcessor.csusing System;using FileProc;namespace BmpProcessor{ public class BmpProcessor: IFileProcessor { override public bool IsSupport(string filePath) { return ".bmp" == IO.Path.GetExtension(filePath).ToLower(); } }}

 

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

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

// compile with csc LateBind.cs /r:FileProcessor.csusing System;using System.IO;using System.Reflection;using FileProc; namespace LateBind{ class Class1 { [STAThread] static void 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 и поместить его в ту же папку, что и остальные классы-обработчики.

Атрибуты

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

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

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

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

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

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

Формулируя алгоритм с помощью абстрактных (виртуальных) методов класса, вы откладываете окончательную реализацию алгоритма на время исполнения программы. Именно тогда, во время исполнения, решается то, какие реальные методы будут участвовать в работе алгоритма. Это зависит от того с какими конкретными объектами придётся работать алгоритму. Можно сказать, что виртуальные методы конкретизируются в 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 { static public 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 { static void 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. Это событие генерируется при любой неудачной попытке загрузить в домен какую-либо сборку. А так как нашей сборки ещё нет (она ещё не сгенерирована), то любая попытка её загрузить будет неудачной. Среди прилагающихся к статье примеров есть соответствующий этому случаю.

Заключение

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

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

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

Источник: RSDN Magazine #2

Опубликовано: 14.03.2003
Исправлено: 05.01.2007
Версия текста: 1.1.1

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

function ToggleCode(id) { el=document.getElementById(id); img=document.getElementById("img"+id); if(el.style.display=="none") { img.src="/images/ls2.gif"; el.style.display=""; } else { img.src="/images/ls1.gif"; el.style.display="none"; } return false; } Исходные тексты примеров(~100 кб)

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

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

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

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



Поделиться:


Последнее изменение этой страницы: 2016-07-14; просмотров: 697; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 3.17.154.171 (0.098 с.)