Может быть, вам нужно эффективное строго типизированное хранилище данных, но это будет очень дорого, если вам придется обновлять схему базы данных каждый раз, когда объектная модель меняет стоимость?
лучше определить схему типов во время выполнения. Вам нужно доставить компоненты, которые принимают произвольные пользовательские объекты и обрабатываютих
каким-либо интеллектуальным способом? Хотите, чтобы компилятор библиотеки мог программно сообщать вам, каковы их типы?
Чтобы поддерживать строго типизированные структуры данных, одновременно обеспечивая максимальную гибкость во время выполнения, вы, вероятно, захотите рассмотреть возможность отражения и того, как оно может улучшить ваше программное обеспечение. В этой статье я расскажу о пространстве имен System.Reflection в Microsoft .NET Framework и о том, какую пользу оно может принести вашему опыту разработки. Я начну с простых примеров и закончу тем, как справляться с реальными ситуациями сериализации. Попутно я покажу, как отражение и CodeDom работают вместе для эффективной обработки данных времени выполнения.
Прежде чем углубиться в System.Reflection, я хотел бы обсудить рефлексивное программирование в целом. Во-первых, отражение можно определить как любую функциональность, предоставляемую системой программирования, которая позволяет программистам проверять объекты кода и манипулировать ими без предварительного знания их личности или формальной структуры. В этом разделе есть что рассказать, поэтому я буду рассматривать его по одному.
Во-первых, что дает рефлексия? Что с ней можно сделать? Я склонен разделять типичные задачи, ориентированные на рефлексию, на две категории: проверка и манипуляция. Проверка требует анализа объектов и типов для сбора структурированной информации об их определении и поведении. За исключением нескольких основных положений, это часто делается без какого-либо предварительного знания о них. (Например, в .NET Framework все наследуется от System.Object, и ссылка на тип объекта часто является общей отправной точкой для отражения.)
Операции динамически вызывают код, используя информацию, собранную посредством проверки, создания новых экземпляров или даже типы и объекты можно легко динамически реструктурировать. Важно отметить, что для большинства систем манипулирование типами и объектами во время выполнения приводит к снижению производительности по сравнению с статическим выполнением эквивалентных операций в исходном коде. Это необходимый компромисс из-за динамической природы отражения, но существует множество советов и рекомендаций по оптимизации производительности отражения (более подробную информацию об оптимизации см. на msdn.microsoft.com/msdnmag/issues/05). использование отражения /07/Reflection).
Итак, какова цель отражения? Что на самом деле проверяет и манипулирует программист? В своем определении отражения я использовал новый термин «объект кода», чтобы подчеркнуть тот факт, что с точки зрения программиста методы отражения иногда стирают границы между ними. традиционные объекты и типы. Например, типичная задача, ориентированная на отражение, может быть следующей:
начать с дескриптора объекта O и использовать отражение для получения дескриптора связанного с ним определения (тип T).
Исследуйте тип T и получите дескриптор его метода M.
Вызвать метод M другого объекта O' (также типа T).
Обратите внимание, что я переключаюсь от одного экземпляра к его базовому типу, от этого типа к методу, а затем использую дескриптор метода для его вызова в другом экземпляре - очевидно, что это использование традиционного программирования на C# в исходном коде. Технология не может этого достичь. После обсуждения System.Reflection .NET Framework ниже я еще раз объясню эту ситуацию на конкретном примере.
Некоторые языки программирования обеспечивают отражение изначально через синтаксис, тогда как другие платформы и фреймворки (например, .NET Framework) предоставляют его в виде системной библиотеки. Независимо от того, каким образом обеспечивается отражение, возможности использования технологии отражения в той или иной ситуации достаточно сложны. Способность системы программирования обеспечивать отражение зависит от многих факторов: хорошо ли программист использует возможности языка программирования для выражения своих концепций, встраивает ли компилятор в выходные данные достаточно структурированной информации (метаданных), чтобы облегчить будущий анализ? Интерпретация? Существует ли подсистема времени выполнения или интерпретатор хоста, которая обрабатывает эти метаданные? Представляет ли библиотека платформы результаты этой интерпретации таким образом, который будет полезен программистам?
Если вы имеете в виду сложную объектно-ориентированную систему типов, но это не так? появляется в коде как простая функция в стиле C, и формальная структура данных отсутствует, то ваша программа, очевидно, не сможет динамически сделать вывод, что указатель определенной переменной v1 указывает на экземпляр объекта определенного типа T . Потому что, в конце концов, тип T — это концепция в вашей голове, она никогда не появляется явно в ваших программных операторах; Но если вы используете более гибкий объектно-ориентированный язык (например, C#) для выражения абстрактной структуры программы и напрямую вводите концепцию типа T, тогда компилятор преобразует вашу идею во что-то, что позже можно будет передать через программу. соответствующую логику для понимания формы, предоставляемую средой CLR или каким-либо интерпретатором динамического языка.
Является ли отражение полностью динамической технологией, работающей во время выполнения? Проще говоря, это не так? На протяжении цикла разработки и выполнения часто бывает, что отражение доступно и полезно разработчикам. Некоторые языки программирования реализуются посредством автономных компиляторов, которые преобразуют высокоуровневый код непосредственно в инструкции, понятные машине. Выходной файл включает только скомпилированные входные данные, а среда выполнения не имеет вспомогательной логики для принятия непрозрачных объектов и динамического анализа их определений. Именно так обстоит дело со многими традиционными компиляторами C. Поскольку в целевом исполняемом файле мало вспомогательной логики, вы не можете выполнять большую часть динамического отражения, но компиляторы время от времени обеспечивают статическое отражение — например, вездесущий оператор typeof позволяет программистам проверять идентификаторы типов во время компиляции.
Совершенно другая ситуация заключается в том, что интерпретируемые языки программирования всегда получают выполнение через основной процесс (в эту категорию обычно попадают скриптовые языки). Поскольку доступно полное определение программы (в виде входного исходного кода) в сочетании с полной языковой реализацией (в виде самого интерпретатора), все методы, необходимые для поддержки самоанализа, имеются в наличии. Этот динамический язык часто предоставляет комплексные возможности отражения, а также богатый набор инструментов для динамического анализа и манипулирования программами.
CLR .NET Framework и его основные языки, такие как C#, находятся посередине. Компилятор используется для преобразования исходного кода в IL и метаданные. Последние имеют более низкий уровень или менее «логичны», чем исходный код, но при этом сохраняют много абстрактной структуры и информации о типах. После того как CLR запустит и разместит эту программу, библиотека System.Reflection библиотеки базовых классов (BCL) сможет использовать эту информацию и возвращать информацию о типе объекта, членах типа, сигнатурах членов и т. д. Кроме того, он также может поддерживать вызовы, включая вызовы с поздним связыванием.
Отражение в .NET
Чтобы воспользоваться преимуществами отражения при программировании с помощью .NET Framework, вы можете использовать пространство имен System.Reflection. Это пространство имен предоставляет классы, которые инкапсулируют многие концепции времени выполнения, такие как сборки, модули, типы, методы, конструкторы, поля и свойства. В таблице на рисунке 1 показано, как классы в System.Reflection сопоставляются со своими концептуальными аналогами во время выполнения.
Хотя это и важно, System.Reflection.Assembly и System.Reflection.Module в основном используются для поиска и загрузки нового кода в среду выполнения. В этой статье я не буду обсуждать эти части и предполагаю, что весь соответствующий код уже загружен.
Для проверки загруженного кода и управления им обычно используется шаблон System.Type. Обычно вы начинаете с получения экземпляра System.Type интересующего класса среды выполнения (через Object.GetType). Затем вы можете использовать различные методы System.Type, чтобы изучить определение типа в System.Reflection и получить экземпляры других классов. Например, если вас интересует конкретный метод и вы хотите получить экземпляр этого метода System.Reflection.MethodInfo (возможно, через Type.GetMethod). Аналогично, если вас интересует поле и вы хотите получить экземпляр этого поля System.Reflection.FieldInfo (возможно, через Type.GetField).
Когда у вас есть все необходимые объекты экземпляра отражения, вы можете продолжить, выполнив шаги по проверке или манипуляции по мере необходимости. При проверке вы используете различные описательные свойства в отражающем классе, чтобы получить необходимую информацию (это универсальный тип? Это метод экземпляра?). В процессе работы вы можете динамически вызывать и выполнять методы, создавать новые объекты путем вызова конструкторов и так далее.
Проверка типов и членов
Давайте перейдем к коду и рассмотрим, как выполнять проверку с использованием базового отражения. Я сосредоточусь на анализе типов. Начав с объекта, я получу его тип, а затем исследую несколько интересных членов (см. рис. 2).
Первое, что следует отметить, это то, что в определении класса, на первый взгляд, кажется, что для описания методов гораздо больше места, чем я ожидал. Откуда берутся эти дополнительные методы? Любой, кто разбирается в иерархии объектов .NET Framework, узнает, что эти методы унаследованы от самого общего базового класса Object. (На самом деле я сначала использовал Object.GetType для получения его типа.) Кроме того, вы можете увидеть функцию получения свойства. А что, если вам нужны только явно определенные функции самого MyClass? Другими словами, как скрыть унаследованные функции? Или, может быть, вам нужны только явно определенные функции экземпляра,
и вы поймете
?Я обнаружил, что каждый готов использовать второй перегруженный метод GetMethods, который принимает параметр BindingFlags. Комбинируя разные значения из перечисления BindingFlags, вы можете заставить функцию возвращать только желаемое подмножество методов. Замените вызов GetMethods на:
GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly |BindingFlags.Public)
В результате вы получите следующий вывод (обратите внимание, что здесь нет статических вспомогательных функций и функций, унаследованных от System.Object).
Демонстрационный пример отражения 1
Имя типа: MyClass
Имя метода: MyMethod1
Имя метода: MyMethod2
Имя метода: get_MyProperty
Имя свойства: MyProperty
Что, если вы заранее знаете имя типа (полное) и члены Как выполнить извлечение из типа перечисления в тип? Преобразование? С кодом в первых двух примерах у вас уже есть базовые компоненты для реализации браузера примитивных классов. Вы можете найти объект времени выполнения по имени, а затем перечислить различные связанные с ним свойства.
Динамический вызов кода.
До сих пор я получал дескрипторы объектов времени выполнения (таких как типы и методы) только для описательных целей, например для печати их имен. Но что еще можно сделать? Как на самом деле вызвать метод?
Вот несколько ключевых моментов в этом примере: сначала экземпляр System.Type извлекается из экземпляра MyClass, mc1, а затем из экземпляра MethodInfo. этот тип. Наконец, когда вызывается MethodInfo, он привязывается к другому экземпляру MyClass (mc2), передавая его в качестве первого параметра вызова.
Как упоминалось ранее, этот пример стирает различие между типами и использованием объектов, которое вы ожидаете увидеть в исходном коде. Логически вы получаете дескриптор метода, а затем вызываете его, как если бы он принадлежал другому объекту. Для программистов, знакомых с функциональными языками программирования, это может быть несложно, но для программистов, знакомых только с C#, разделение реализации объекта и создания экземпляра объекта может быть не столь интуитивно понятным;
Собираем все вместе
До сих пор я обсуждал основные принципы проверки и колла, а теперь сопоставлю их с конкретными примерами. Представьте, что вы хотите предоставить библиотеку со статическими вспомогательными функциями, которые должны обрабатывать объекты. Но во время разработки вы не имеете никакого представления о типах этих объектов! От инструкций вызывающего функции зависит, как он хочет извлечь значимую информацию из этих объектов. Функция будет принимать коллекцию объектов и строковый дескриптор метода. Затем он будет перебирать коллекцию, вызывая методы каждого объекта и агрегируя возвращаемые значения с помощью некоторой функции.
В этом примере я собираюсь объявить некоторые ограничения. Во-первых, метод, описываемый строковым параметром (который должен быть реализован базовым типом каждого объекта), не принимает никаких параметров и возвращает целое число. Код будет перебирать коллекцию объектов, вызывая указанный метод, и постепенно вычислять среднее значение всех значений. Наконец, поскольку это не рабочий код, мне не нужно беспокоиться о проверке параметров или переполнении целых чисел при суммировании.
Просматривая пример кода, вы можете видеть, что соглашение между основной функцией и статическим помощником ComputeAverage не опирается на какую-либо информацию о типе, кроме общего базового класса самого объекта. Другими словами, вы можете полностью изменить тип и структуру передаваемого объекта, но пока вы всегда можете использовать строку для описания метода, возвращающего целое число, ComputeAverage будет работать нормально.
Ключевой момент, на который следует обратить внимание, заключается в том, что он
работает нормально!скрыт в. Последний пример связан с MethodInfo (общее отражение). Обратите внимание, что в цикле foreach ComputeAverage код извлекает MethodInfo только из первого объекта в коллекции, а затем привязывает его к вызову для всех последующих объектов. Как показывает кодировка, работает нормально — это простой пример кэширования MethodInfo. Но здесь есть фундаментальное ограничение. Экземпляр MethodInfo может быть вызван только экземпляром того же иерархического типа, что и извлекаемый им объект. Это возможно, поскольку передаются экземпляры IntReturner и SonOfIntReturner (унаследованные от IntReturner).
В пример кода включен класс с именем EnemyOfIntReturner, который реализует тот же базовый протокол, что и два других класса, но не имеет общих общих типов. Другими словами, интерфейсы логически эквивалентны, но на уровне типов нет перекрытия. Чтобы изучить использование MethodInfo в этой ситуации, попробуйте добавить в коллекцию еще один объект, получите экземпляр с помощью «new EnemyOfIntReturner(10)» и запустите пример еще раз. Вы столкнетесь с исключением, указывающим, что MethodInfo нельзя использовать для вызова указанного объекта, поскольку он не имеет абсолютно ничего общего с исходным типом, из которого был получен MethodInfo (даже несмотря на то, что имя метода и базовый протокол эквивалентны). Чтобы подготовить свой код к производству, вы должны быть готовы к такой ситуации.
Возможным решением может быть анализ типов всех входящих объектов самостоятельно, сохраняя интерпретацию их общей иерархии типов (если таковая имеется). Если тип следующего объекта отличается от любой известной иерархии типов, необходимо получить и сохранить новую информацию о методе. Другое решение — перехватить TargetException и повторно получить экземпляр MethodInfo. Оба упомянутых здесь решения имеют свои плюсы и минусы. Джоэл Побар написал отличную статью для майского номера этого журнала за 2007 год о производительности буферизации и отражения MethodInfo, которую я настоятельно рекомендую.
Будем надеяться, что этот пример демонстрирует добавление отражения в приложение или платформу, чтобы добавить больше гибкости для будущей настройки или расширения. Следует признать, что использование отражения может быть немного громоздким по сравнению с эквивалентной логикой в собственных языках программирования. Если вы чувствуете, что добавление позднего связывания на основе отражения в ваш код слишком обременительно для вас или ваших клиентов (в конце концов, им нужно, чтобы их типы и код каким-то образом учитывались в вашей структуре), то это может быть необходимо только для обеспечения гибкости модерации. для достижения некоторого баланса.
Эффективная обработка типов для сериализации
Теперь, когда мы рассмотрели основные принципы отражения .NET на нескольких примерах, давайте взглянем на реальную ситуацию. Если ваше программное обеспечение взаимодействует с другими системами через веб-службы или другие внепроцессные технологии удаленного взаимодействия, вы, вероятно, столкнулись с проблемами сериализации. Сериализация по существу преобразует активные объекты, занимающие память, в формат данных, подходящий для онлайн-передачи или хранения на диске.
Пространство имен System.Xml.Serialization в .NET Framework предоставляет мощный механизм сериализации с XmlSerializer, который может взять любой управляемый объект и преобразовать его в XML (данные XML также можно преобразовать обратно в экземпляр типизированного объекта в будущем. Этот процесс называется десериализацией). Класс XmlSerializer — это мощное, готовое к использованию на предприятии программное обеспечение, которое станет вашим первым выбором, если вы столкнетесь с проблемами сериализации в своем проекте. Но в образовательных целях давайте рассмотрим, как реализовать сериализацию (или другие подобные экземпляры обработки типов во время выполнения).
Учтите следующее: вы предоставляете платформу, которая берет экземпляры объектов произвольных типов пользователей и преобразует их в некоторый интеллектуальный формат данных. Например, предположим, что у вас есть резидентный объект типа Address, как показано ниже:
(псевдокод)
адрес класса
{
идентификатор адреса;
Стринг-стрит, город;
StateType Состояние;
Почтовый индексТипПочтовый индекс;
}
Как создать подходящее представление данных для дальнейшего использования Возможно, эту проблему решит простая визуализация текста:
Адрес: 123
Street: 1 Microsoft Way
Город: Редмонд
Штат: WA
Почтовый индекс: 98052
Если формальные данные, которые необходимо преобразовать, полностью понятны? введите заранее (например, при написании кода самостоятельно), все становится очень просто:
foreach(Address a in AddressList)
{
Console.WriteLine("Адрес:{0}", a.ID);
Console.WriteLine("tStreet:{0}", a.Street);
... // и так далее
}
Однако все может стать действительно интересным, если вы заранее не знаете, с какими типами данных вы столкнетесь во время выполнения. Как написать такой общий код платформы
MyFramework.TranslateObject(ввод объекта, вывод MyOutputWriter)
Во-первых, вам нужно решить, какие члены типа полезны для сериализации. Возможности включают в себя захват только членов определенного типа, таких как примитивные системные типы, или предоставление авторам типа механизма для указания того, какие члены необходимо сериализовать, например использование пользовательских свойств в качестве маркеров для членов типа). Вы можете захватывать только элементы определенного типа, например примитивные системные типы, или автор типа может указать, какие элементы необходимо сериализовать (возможно, используя пользовательские свойства в качестве маркеров на членах типа).
После того как вы задокументировали элементы структуры данных, которые необходимо преобразовать, вам нужно написать логику для их перечисления и извлечения из входящих объектов. Отражение выполняет здесь тяжелую работу, позволяя запрашивать как структуры данных, так и значения данных.
Для простоты давайте разработаем облегченный механизм преобразования, который берет объект, получает все значения его общедоступных свойств, преобразует их в строки путем прямого вызова ToString, а затем сериализует значения. Для данного объекта с именем «вход» алгоритм примерно следующий:
вызовите input.GetType, чтобы получить экземпляр System.Type, который описывает базовую структуру ввода.
Используйте Type.GetProperties и соответствующий параметр BindingFlags для получения общедоступных свойств в виде экземпляров PropertyInfo.
Свойства извлекаются как пары ключ-значение с помощью PropertyInfo.Name и PropertyInfo.GetValue.
Вызовите Object.ToString для каждого значения, чтобы преобразовать его (простым способом) в строковый формат.
Упакуйте имя типа объекта и коллекцию имен свойств и строковых значений в правильный формат сериализации.
Этот алгоритм значительно упрощает задачу, а также учитывает суть использования структуры данных времени выполнения и превращения ее в самоописывающиеся данные. Но есть проблема: производительность. Как упоминалось ранее, отражение очень затратно как для обработки типов, так и для извлечения значений. В этом примере я выполняю полный анализ типов для каждого экземпляра предоставленного типа.
Что, если бы можно было каким-то образом уловить или сохранить ваше понимание структуры типа, чтобы вы могли легко извлечь ее позже и эффективно обрабатывать новые экземпляры этого типа, другими словами, перейдите к шагу №3 в примере алгоритма? Новость заключается в том, что это можно сделать, используя функции .NET Framework. Как только вы поймете структуру данных типа, вы сможете использовать CodeDom для динамического создания кода, который привязывается к этой структуре данных. Вы можете создать вспомогательную сборку, содержащую вспомогательный класс и методы, которые ссылаются на входящий тип и напрямую обращаются к его свойствам (как и к любому другому свойству в управляемом коде), поэтому проверка типов влияет на производительность только один раз.
Сейчас исправлю этот алгоритм. Новый тип:
получите экземпляр System.Type, соответствующий этому типу.
Используйте различные методы доступа System.Type для получения схемы (или, по крайней мере, ее подмножества, полезного для сериализации), например имен свойств, имен полей и т. д.
Используйте информацию о схеме для создания вспомогательной сборки (через CodeDom), которая связывается с новым типом и эффективно обрабатывает экземпляры.
Используйте код во вспомогательной сборке для извлечения данных экземпляра.
Сериализуйте данные по мере необходимости.
Для всех входящих данных заданного типа вы можете перейти к шагу №4 и получить огромный прирост производительности по сравнению с явной проверкой каждого экземпляра.
Я разработал базовую библиотеку сериализации под названием SimpleSerialization, которая реализует этот алгоритм с использованием отражения и CodeDom (можно загрузить в этой колонке). Основным компонентом является класс SimpleSerializer, который создается пользователем с помощью экземпляра System.Type. В конструкторе новый экземпляр SimpleSerializer анализирует заданный тип и генерирует временную сборку с использованием вспомогательных классов. Вспомогательный класс тесно привязан к данному типу данных и обрабатывает экземпляр так, как если бы вы писали код с полным предварительным знанием типа.
Класс SimpleSerializer имеет следующую структуру:
класс SimpleSerializer.
{
общедоступный класс SimpleSerializer (Тип dataType);
public void Serialize (ввод объекта, писатель SimpleDataWriter);
}
Просто потрясающе! Конструктор выполняет тяжелую работу: он использует отражение для анализа структуры типов, а затем использует CodeDom для создания вспомогательной сборки. Класс SimpleDataWriter — это просто приемник данных, используемый для иллюстрации общих шаблонов сериализации.
Чтобы сериализовать простой экземпляр класса Address, используйте следующий псевдокод для выполнения задачи:
SimpleSerializer mySerializer = new SimpleSerializer(typeof(Address));
SimpleDataWriter Writer = new SimpleDataWriter();
mySerializer.Serialize(addressInstance
, Writer
End
).рекомендуется самостоятельно опробовать пример кода, особенно библиотеку SimpleSerialization. Я добавил комментарии к некоторым интересным частям SimpleSerializer, надеюсь, это поможет. Конечно, если вам нужна строгая сериализация в рабочем коде, вам действительно придется полагаться на технологии, представленные в .NET Framework (например, XmlSerializer). Но если вы обнаружите, что вам нужно работать с произвольными типами во время выполнения и эффективно обрабатывать их, я надеюсь, что вы воспользуетесь моей библиотекой SimpleSerialization в качестве решения.