Сообщений 4    Оценка 615        Оценить  
Система Orphus

R# – метапрограммирование в .NET

Автор: Владислав Чистяков
The RSDN Team

Источник: RSDN Magazine #5-2004
Опубликовано: 23.05.2005
Исправлено: 16.11.2005
Версия текста: 1.0
Что такое R#
Декларативное программирование
Метапрограммирование
Метаобъекты
АОП
Почему "#"
Практический пример
Паттерн Посетитель (Visitor)
Компилятор R#
Сначала терминология
Создание проекта, содержащего метаправило
Что же происходит при запуске rsc.exe
Правила генерации кода для R#
Синтаксис XPath-запросов
Meta ID
Контекстная замена текстовых полей в AST
Глубокое клонирование
Работа с атрибутами
Метаправило для реализации паттерна Посетитель (Visitor) на R#
Список проектов
Как подключиться к проекту?
Как скачать архив с исходниками?
Как подключиться к базе subversion?
Как скомпилировать проект?
Что требуется для компиляции?
Лицензия R#
Планы

Исходный код R#

Что такое R#

R# - это попытка расширить возможности C# (а возможно, и других языков) путем введения в него средств метапрограммирования. Почему именно метапрограммирования и почему именно в C#, будет сказано несколько ниже, а здесь я попытаюсь описать, что это такое и как оно работает.

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

Для реализации этой идеи были созданы следующие компоненты:

  1. Синтаксический и лексический анализаторы C#. Парсер принимает на вход текст, синтаксически (но не обязательно семантически) совместимый с C# 2.0 (спецификацию можно взять здесь: http://download.microsoft.com/download/8/1/6/81682478-4018-48fe-9e5e-f87a44af3db9/Standard.pdf). В процессе разбора исходного кода парсер строит абстрактное синтаксическое дерево (Abstract Syntax Tree, AST). Это дерево можно изменять путем замены (изменения), удаления или добавления узлов. Каждый узел дерева представляет некоторую синтаксическую конструкцию. Например, конструкция: «while (true)» разбирается в дерево, состоящее из оператора (Statement) while и выражения (Expression) true, вложенного в оператор while. Сам по себе оператор while может быть вложен в другой оператор или некоторый метод/свойство.
  2. API, позволяющее упростить поиск AST-веток. В качестве языка запросов используется XPath.
  3. API, позволяющее модифицировать AST.
  4. Генератор кода, позволяющий преобразовать AST в код того или иного языка программирования. На сегодня имеется генератор только в C#, но имеется возможность создавать генераторы для любых языков программирования.
  5. Компилятор языка R#, использующий возможности, описанные в предыдущих пунктах. Он вводит понятие метаправила и метасборки (о которых будет подробно рассказано ниже), позволяющие расширять возможности R#, не внося изменений в код самого компилятора. В дальнейшем, возможно, он будет обеспечивать генерацию MSIL, избавляя от необходимости пользоваться внешним компилятором C#. Сейчас же он работает как синтаксический препроцессор, выдавая в качестве результата код, удовлетворяющий стандарту C# 2.0.

Описанные выше средства решают прямые задачи проекта R#. Однако R# планировался и создавался как модульное решение, поэтому все его части можно использовать как независимые библиотеки. Даже самый специализированный компонент, компилятор, создан как библиотека, которую можно использовать из других приложений, и загрузочный модуль, позволяющий использовать эту библиотеку из командной строки. Парсер и API запросов можно использовать для получения информации об обычных C#-проектах, так как синтаксис R# и C# полностью совместимы. Например, на базе этого парсера можно создавать программы проверки корпоративных требований к кодированию и поиска часто встречающихся ошибок (то есть создать аналог FxCop, отталкивающийся не от скомпилированного модуля, а от исходного текста программы). Возможности использования компонентов R# весьма обширны, так, на их базе можно создавать функциональные и логические расширения C#, синтаксические оптимизаторы, программы интеллектуального рефакторинга, профайлеры, преобразователи в другие языки (например, в MC++ или VB.NET), визуальные дизайнеры классов и т.п.

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

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

Чтобы сделать программу максимально прозрачной и уменьшить количество ошибок, нужно поднимать уровень абстракции. Наивысший уровень абстракции – это декларация. Так уж повелось, что развитие пошло по пути императивного программирования. И хотя имеется множество направлений наподобие Пролога или функциональных языков, по тем или иным причинам они не стали господствующей тенденцией в программировании. Основным средством повышения абстракции на сегодня стало ООП и связанная с ним объектно-ориентированная идеология. К сожалению, не всегда его средствами можно достичь того уровня декларативности, который хотелось бы иметь.

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

Метапрограммирование

Что такое метапрограммирование?

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

Иными словами метапрограммирование – это процесс автоматизации создания ПО. Зачем это нужно? Довольно часто в практике программирования встречаются моменты, когда программист знает оптимальный путь решения той или иной задачи, но этот путь связан с большим объемом кодирования. Причем объем связан с тем, что имеется множество одинаковых мест, где нужно создавать очень похожий, но зависящий от контекста код.

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

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

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

Такую модель можно представить в виде некоторой метаинформации, или метаданных. Метаданные – это данные о программе, или, в более общем случае данные о данных. Потенциально можно создать программу, способную породить некий код, опираясь только на мысли разработчика или проектировщика, который держит всю информацию в собственной голове. Но таким образом можно создать только очень примитивные решения. Что-либо более сложное требует фиксирования метаинформации в той или иной форме.

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

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

Порой метапрограммирование можно обнаружить в местах, где его наличие трудно предполагать. Например, динамические Web-страницы (JSP, ASP.NET и т.п.) по сути тоже являются некоторым метаязыком, на базе которого процессоры этих систем генерируют исходный код класса Web-страницы, который, в свою очередь, генерирует HTML, выдаваемый пользователю.

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

Сегодня видны робкие попытки создать такие средства. Среди них можно назвать такие продукты, как OpenC++ или описываемый в данной статье R#. Стоит сказать также о развивающейся в последнее время концепцию Аспектно-Ориентированного Программирования (АОП).

До этого момента речь шла в основном об упрощении жизни программистов. Но архитекторы приложений, в принципе, рады были бы обойтись и вовсе без программистов, своими кривыми руками искажающих их гениальные планы. Уже очень давно существует идеология CASE-средств, позволяющая дизайнерам строить модели, которые в дальнейшем (в худшем случае) попадают к программистам. А в лучшем случае по этим моделям генерируется код, который затем доводится до работоспособного состояния. В процессе доводки программистам часто удается уделать эту модель настолько, что даже если ее удастся методами обратного инжиниринга превратить в состояние модели, то архитектор ее, мягко говоря, не узнает. Поэтому зачастую, побаловавшись CASE-средствами на начальном этапе разработки, фирмы-разработчики плюют на них и продолжают работать по старинке. Чтобы сблизить архитектора и программиста, все чаще применяются паттерны проектирования. Это дает архитектору если не уверенность, то надежду на то, что его архитектура будет воплощена так, как он и рассчитывал. Ведь для архитектора паттерн – это строительный блок, а для программиста – четкая инструкция по реализации.

Есть паттерны, которые относительно легко вписываются в концепции ООП или обобщенного программирования (шаблоны C++, generic-и .NET и Java), позволяя создать базовый класс, реализующий необходимую функциональность. Но многие паттерны плохо реализуются этими средствами и требуют больших трудозатрат как на реализацию, так и на поддержку. АОП и метапрограммирование позволяют автоматизировать реализацию паттернов, доводя их практически до декларативного уровня.

Метаобъекты

Метаобъектами называют сущности, выражающие некоторые аспекты объектов – типы, интерфейсы, классы, методы, атрибуты, переменные, функции, управляющие структуры кода и т.п.

Metaobject Protocol (MOP) – это обобщенный способ оперировать группой объектов как целым. Он может оказаться полезным при реализации обобщенных функций.

MOP – это набор классов и методов, позволяющих программе исследовать состояние и изменять поведение поддерживаемой программы. Хорошее описание MOP, базирующееся на Common Lisp Object System (CLOS), позволяющем манипулировать механизмами наследования, диспетчеризации методов, порождением классов и т.д., можно найти в книге "The Art of the Metaobject Protocol".

Идея метаобъектов была применена в языке, который так и не получил широкого распространения. К сожалению, большинство заложенных в нем идей до сих пор не стали достоянием масс. R# является попыткой привнести в мир императивного C#-программирования некоторые средства, схожие с метаобъектами.

Отсутствие решений, подобных метаобъектам, в языках типа Java и C# – стало один из мотивов разработки аспектно-ориентированного программирования.

АОП

АОП – это довольно новая парадигма программирования. В отличие от других, она не конкурирует с такими общепринятыми парадигмами, как процедурное программирование (ПП) или объектно-ориентированное программирование (ООП). АОП – это всего лишь еще один взгляд на дизайн и производство ПО.

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


В простейшем случае аспект – это просто кусок кода, который может быть подставлен технологией copy/paste или контекстной заменой. Многие реализации АОП останавливаются на этом примитивном уровне. Однако иногда желательно, чтобы код аспекта мог изменяться (модифицироваться) в зависимости от контекста, к которому он применяется. Естественно, что такая модификация может быть выполнена средствами метапрограммирования. Стало быть, АОП не только может быть эффективно реализовано с помощью метапрограммирования, но и более того, метапрограммирование может гармонично дополнять эту концепцию.

Таким образом, можно рассматривать идеи, заложенные в АОП, как средство структуризации идей метапрограммирования, а само АОП – как одно из его возможных проявлений.

Метапрограммирование является прекрасным средством воплощения идей АОП в жизнь, но не единственным. Альтернативой метапрограммированию может послужить встраивание поддержки АОП непосредственно в языки программирования (но это негибкое решение). Другой вариант – реализация его в runtime средствами генерации некоторых оберток, дополняющих функциональность объекта, или перехват выполнения средствами runtime того или иного языка (например, CBO-объекты в .NET). Эти методы повышают гибкость, так как решение о перехвате управления или изменении поведения можно принимать во время исполнения, однако при этом они способны существенно снизить производительность программы.

Введение в АОП

Игорь Ткачев

АСПЕКТ (от лат. aspectus — вид), точка зрения, с которой рассматривается какое-либо явление, понятие, перспектива. (Большой энциклопедический словарь)

Исследователи изучили различные пути выделения в отдельные модули сквозной функциональности в сложных программных системах. Аспектно-ориентированное программирование (АОП) является одним из этих решений. АОП предлагает средства выделения сквозной функциональности в отдельные программные модули — аспекты.

С точки зрения АОП в процессе разработки достаточно сложной системы программист решает две ортогональные задачи:

- Разработка компонентов, то есть выявление классов и объектов, составляющих словарь предметной области.

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

Современные языки программирования (такие как, например, C++, VB и т.п.) ориентированы, прежде всего, на решение первой задачи. Код компонента представляется в виде класса, т.е. он хорошо локализован и, следовательно, его легко просматривать, изучать, модифицировать, повторно использовать. С другой стороны, при программировании процессов, в которые вовлечены различные объекты, мы получаем код, в котором элементы, связанные с поддержкой такого процесса, распределены по коду всей системы. Эти элементы встречаются в коде множества классов, их совокупность в целом не локализована в обозримом сегменте кода. В результате мы сталкиваемся с проблемой "запутанного" кода.

Почему "#"

Знак # в названии R#, очевидно, намекает на связь с C#. Это не случайно. R# базируется на C#. По сути, задачей R# было расширение возможностей C#. R# наделяет C# средствами метапрограммирования. На свете существует немало интересных и популярных языков программирования, поэтому, возможно, имеет смысл объяснить, почему мы выбрали именно C#.

Что мы получили в лице C#, и чего в нем не хватает?

Что мы имели до C#? По большому счету много чего. Были и функциональные языки, и ассемблер, и компонентные технологии вроде COM, но думаю, что многие согласятся, что основная масса программистов использовала в работе C++ и/или Delphi. По сравнению с Delphi (в данном случае речь идет о Delphi версии 7 и меньше) при переходе на C# программист практически ничего не теряет, приобретая при этом:

  1. Полную типобезопасность (в safe-режиме).
  2. Более высокоуровневые конструкции вроде делегатов, событий, атрибутов, встроенной поддержки хеширования и т.п.
  3. Автоматическое управление памятью.
  4. Более гибкое управление областями видимости (на основе пространств имен).
  5. Парадигму перечислителей (enumerator), встроенную во все базовые типы, поддерживаемую на уровне языка оператором foreach и дополненную в C# 2.0 красивой концепцией итераторов.
  6. Стандартизированную и хорошо документированную информацию о типах, доступную во время исполнения. В Delphi тоже есть RTTI, но оно плохо документировано и не имеет такого удобного способа расширения, как атрибуты.
  7. Отсутствие необходимости предварительной декларации.
  8. Автодокументирование.
  9. В C# 2.0 появились дополнительные расширения (generic-и, анонимные методы и многое другое).

При этом скорость компиляции сопоставимых проектов ничуть не хуже, а общее время разработки – меньше. .NET также предоставляет прекрасные компонентные возможности, которые как минимум не хуже чем в Delphi.

Если проводить сравнение с C++, то можно обнаружить, что с одной стороны, C# выигрывает по многим параметрам, но с другой – кое-чего в нем все-таки нет. Вот список возможностей, отсутствующих в C#, но присутствующих в C++:

  1. Макросы, включение файлов и другие расширенные возможности препроцессора.
  2. Поддержка константности (неизменяемости) для локальных переменных, инициализируемых динамически, параметров и тел функций.
  3. Множественное наследование (далее МН).
  4. Ручной inlining.
  5. Детерминированная финализация (деструкторы).
  6. В C# ограничены возможности перегрузки операторов. Так, нельзя перегружать оператор «=» (присвоения), операторы сдвига «<<» и «>>» обязаны иметь в качестве второго параметра тип int (а значит, их уже нельзя использовать не по назначению, например, для потокового вода/вывода) и т.п.
  7. И, наконец, шаблоны.

По-моему, все, кроме шаблонов и МН – небольшая потеря. Расширенные возможности препроцессора не столько приобретение, сколько источник неприятностей – они порождают вреда намного больше, чем пользы, так что в том виде, в котором они присутствуют в C++, они явно не нужны.

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

По сути, подключение реализации, которое дает нам МН, можно реализовать банальным копированием методов в конечный класс (то есть расширением языка так называемыми mixin-ами). Однако mixin-ов в современном C# нет. В результате все сводится к пресловутой copy-paste-технологии, о которой так нелестно отзываются очень многие опытные программисты. Проблемы очевидны. Появляется большое количество по сути идентичных исходников, что резко усложняет их развитие и поддержку. Например, после стократного копирования (с контекстной заменой) некоторого алгоритма может оказаться, что в исходной версии была ошибка. Это приводит к тому, что нужно модифицировать все копии, что трудоемко и чревато ошибками.

Шаблоны в C++ – вообще статья особая. С одной стороны – это средство обобщенного программирования, позволяющее повысить эффективность программирования, а также повысить надежность и скорость ПО за счет усиления контроля типов во время компиляции. С другой стороны, шаблоны обладают некоторыми, хотя и очень ограниченными возможностями метапрограммирования или, по-другому, возможностями генерации кода. Шаблоны позволили практически создать из C++ другой язык. И хотя многие возможности, создаваемые на базе шаблонов, реализованы в C# штатно, а некоторые (вроде smart-указателей) вообще не нужны в силу особенностей языка и рантайма, все же трудно отрицать, что шаблоны могли бы существенно расширить выразительные возможности C#.

Вопросы, связанные с типобезопасностью, предоставляемой шаблонами вообще теряют актуальность с выходом C# 2.0, так как generic-и, входящие в его состав, предоставляют те же возможности, при этом даже лучше интегрируясь с идеями ООП. Шаблоны C++ зачастую подталкивают программиста к не объектно-ориентированному образу мышления. А такие вещи, как generic-делегаты или generic-интерфейсы позволяют создавать чистые обобщенные абстракции, делая более стройной архитектуру приложения.

Если проанализировать приведенное выше сравнение, то нетрудно заметить, что в C# больше всего не хватает тех особенностей шаблонов, что позволяют заниматься метапрограммированием. С одной стороны, C# более чем пригоден для императивного решения задач. С другой стороны, он имеет расширяемый декларативный механизм – атрибуты. Однако решать все задачи императивно трудно (трудоемко), а использование декларативного механизма сковывается тем, что отрабатывать семантику, закладываемую атрибутами, приходится во время исполнения программы. Это, с одной стороны, довольно сложно, а с другой – медленно (то есть порождает медленный код).

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

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

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

ПРИМЕЧАНИЕ

Почему с некоторой натяжкой? Дело в том, что C++ спроектирован так, что не все его синтаксические конструкции можно правильно распознать, не имея информации о том, чем является тот или иной идентификатор. Например, конструкция:

f(i);

рассматриваемая в контексте тела некоторой функции, может интерпретироваться как определение переменной i типа f, если f – это тип данных, или как вызов функции f, которой передается параметр i, если f – это не тип данных. Это не вызывает проблем у компилятора при разборе обычного C++-кода, так как все типы данных должны быть обязательно объявлены выше по тексту. Однако в шаблонах это может стать проблемой. До подстановки фактических параметров не всегда можно уверенно сказать, чем является тот или иной идентификатор.

При применении шаблонов C++ некоторые фокусы позволяют заниматься генерацией кода. К сожалению, это не основная функция шаблонов. Она не была предусмотрена создателями языка. Скорее это использование побочного эффекта в «личных» целях. Однако даже это позволило создать на базе шаблонов такие сложные вещи, как парсеры LL(1)-совместимых языков и механизм регулярных выражений (входящие в библиотеку boost). Причем, в отличие от тех же регулярных выражений из .NET FCL, все выражения компилируются в машинный код самим компилятором C++, и во время исполнения никаких подготовительных действий производить не нужно. К сожалению, сложность реализации подобного рода решений с помощью шаблонов неимоверно велика, а получаемый результат не всегда полностью удовлетворителен. К тому же применение сложной композиции шаблонов зачастую приводит к тому, что диагностика, выдаваемая компиляторами (даже самыми лучшими), может поставить в тупик даже очень опытного программиста, а неопытного просто доведет до истерики.

Все вышесказанное касалось скорее преимуществ C++, так как аналогичных возможностей в C# просто нет. Но C++ обладает и рядом серьезных недостатков. Большинство из них проистекает из того, что создатели языка слишком большое внимание уделяли обратной совместимости с С и слишком стремились избежать снижения производительности получаемого кода. Это породило целую кучу проблем, которыми справедливо укоряют C++ в различных "дискуссиях" типа "C++ vs. ЧтоТоТам". Первейшими претензиями является слабая типобезопасность, заставляющая программиста постоянно заниматься аутотренингом, и отсутствие модульности, приводящее к значительному замедлению компиляции и проблемам в построении разных средств автоматизации труда программиста, основанных на парсинге текста (IntelliSense, рефакторинг и т.п.). Кроме того, в C++ отсутствуют некоторые полезные конструкции вроде событий, делегатов, интерфейсов, анонимных методов и т.п. В основном все они могут эмулироваться в виде библиотечных классов или паттернов проектирования, однако интеграция этих вещей в язык упростила бы жизнь программистов и проектировщиков.

C# 2.0 лишен большинства недостатков C++, он является очень неплохим императивным языком программирования, имеющим, по большому счету, один основной недостаток – отсутствие средств метапрограммирования, позволяющих расширять его возможности и автоматизировать кодирование.

Практический пример

Чтобы было понятнее, попытаюсь продемонстрировать возможности R# на практическом примере. В качестве такого примера будет выступать автоматизация реализации паттерна Посетитель (Visitor).

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

Паттерн Посетитель (Visitor)

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

Например, предположим, что у нас есть множество объектов, описывающих геометрические фигуры: круг (Circle), треугольник (Triangle) и прямоугольник (Rectangle). Все они являются потомками типа "фигура" (IFigure). Можно вычислить площадь (Area) или, скажем, периметр (Perimeter) каждой из них. На C# список этих фигур будет выглядеть следующим образом:

using System;

interface IFigure
{
  double Area();
  double Perimeter();
}

class Circle : IFigure
{
  public double Area()
  {
    return /* вычисление площади круга */;
  }

  public double Perimeter()
  {
    return /* вычисление периметра круга */;
  }

  // Переменные, определяющие круг...
}

class Triangle : IFigure
{
  public double Area()
  {
    return /* вычисление площади треугольника */;
  }

  public double Perimeter()
  {
    return /* вычисление периметра треугольника */;
  }

  // Переменные, определяющие треугольник...
}

class Rectangle : IFigure
{
  public double Area()
  {
    return /* вычисление площади прямоугольника */;
  }

  public double Perimeter() 
  {
    return /* вычисление периметра прямоугольника */;
  }

  // Переменные, определяющие прямоугольник...
}

Каждый класс, реализующий интерфейс IFigure, реализует определенные в нем методы Area и Perimeter по-своему. При этом мы можем работать с множеством фигур, не обращая внимания на их реальный тип. Например, мы можем создать функции, подсчитывающие суммарную площадь или периметр:

double SumArea(IFigure[] figures)
{
  double result = 0;
  foreach (IFigure figure in figures)
    result += figure.Area();

  return result;
}

double SumPerimeter(IFigure[] figures)
{
  double result = 0;
  foreach (IFigure figure in figures)
    result += figure.Perimeter();

  return result;
}

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

IFigure[] figure = new IFigure[]
{
  new Circle(...),
  new Triangle(...),
  new Rectangle(...),
  new Rectangle(...),
  new Triangle(...),
};

Console.WriteLine("Суммарная площадь всех фигур равна " + SumArea(figure));
Console.WriteLine("Сумма периметров всех фигур равна "
  + SumPerimeter(figure));

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

Паттерн Посетитель как раз и предоставляет такое решение. Смысл этого паттерна заключается в том, что обработка выносится в отдельные классы. Такие классы называются Посетителями (Visitor), и их может быть сколько угодно. Для обработки каждого класса из множества в Посетителях должно присутствовать по одному методу Visit.

Чтобы сделать классы множества независимыми от Посетителей, создается интерфейс Посетитель (или абстрактный базовый класс в языках, не поддерживающих интерфейсы). Такой интерфейс обычно называют IXxxVisitor, где Xxx – название базового класса посещаемых классов, или другое имя, идентифицирующее множество обрабатываемых объектов. Этот интерфейс содержит по одному методу для каждого класса, входящего в множество.

Для приведенного выше примера можно создать интерфейс IFigureVisitor, содержащий по одному методу для каждого класса, входящего в множество:

interface IFigureVisitor
{
  void Visit(Circle value);
  void Visit(Triangle value);
  void Visit(Rectangle value);
}

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

interface IFigure
{
  void AcceptVisitor(IFigureVisitor visitor);
}

А его реализация во всех классах множества будет выглядеть так:

class Rectangle : IFigure
{
  public void AcceptVisitor(IFigureVisitor visitor);
  {
    visitor.Visit(this);
  }
  ...
}

Обратите внимание, что все методы интерфейса Посетителя называются Visit. Так что, будучи помещенными в классы посещаемого множества, методы AcceptVisitor будут вызвать соответствующие перегруженные методы. В принципе, методы интерфейса Посетителя можно было называть по-разному, но это уже не существенно. Главное, чтобы вы четко понимали, что происходит при вызове visitor.Visit(this).

Классы, реализующие интерфейс Посетитель, называют Конкретными Посетителями. Таким образом, появляется возможность создания любого количества Посетителей. При этом их создание не влияет на исходный набор классов, и можно создавать любое количество Конкретных Посетителей. То, что Посетители являются отдельными классами и не изменяют объектную модель исходного множества, позволяет даже выделять Посетителей в отдельные модули (сборки), а стало быть, позволяет перепоручить создание Посетителей сторонним разработчикам, например, оформляя их в виде plug-in'ов.

В приведенном ниже примере реализованы два Посетителя, SumArea, подсчитывающий сумму площадей фигур, и SumPerimeter, подсчитывающий сумму длин сторон фигур, причем алгоритмы перебора вынесены в базовый класс Sum, реализующий интерфейс Посетителя:

class abstract Sum : IFigureVisitor
{
  protected double _result;

  public Calculate(IFigure[] figures)
  {
    _result = 0;
    foreach (IFigure figure in figures)
      figure.AcceptVisitor(this);
    return _result;
  }

  public abstract void Visit(Circle value);
  public abstract void Visit(Triangle value);
  public abstract void Visit(Rectangle value);
}

class SumArea : Sum
{

  public override void Visit(Circle value)
  {
      _result += /* вычисление площади круга */;
  }

  public override void Visit(Triangle value)
  {
      _result += /* вычисление площади треугольника */;
  }

  public override void Visit(Rectangle value)
  {
      _result += /* вычисление площади прямоуголника */;
  }
}

class SumPerimeter : Sum
{

  public override void Visit(Circle value)
  {
      _result += /* вычисление периметра круга */;
  }

  public override void Visit(Triangle value)
  {
      _result += /* вычисление периметра треугольника */;
  }

  public override void Visit(Rectangle value)
  {
      _result += /* вычисление периметра прямоуголника */;
  }
}

Применять этих Посетителей можно следующим образом:

SumArea SumArea = new SumArea();
SumPerimeter sumPerimeter = new SumPerimeter();

Console.WriteLine("Суммарная площадь всех фигур равна " 
  + SumArea.Calculate(figure);
Console.WriteLine("Сумма периметров всех фигур равна "
  + sumPerimeter.Calculate(figure);

Чем больше типов входит в список посещаемых, и чем больше Посетителей нужно реализовать, тем более трудоемким становится этот процесс. Нетрудно заметить, что при большом объеме эта работа хорошо алгоритмизуется. Процесс ручной реализации этого паттерна – вещь рутинная, так и напрашивающаяся на автоматизацию. Реализацию интерфейса Visitor в конкретных Посетителях можно автоматизировать средствами VS .NET (и то только в первый раз). Но создание самого интерфейса Visitor и распихивание сотен методов в сотни классов посещаемого множества – задача, не поддающаяся примитивным средствам автоматизации. Если же вспомнить о поддержке такого чудища, то желание связываться с ним несколько ослабеет.

Как же будет выглядеть реализация паттерна Посетитель с применением R#?

При наличии соответствующего метаправила для реализации паттерна Посетитель достаточно в любом файле проекта указать глобальный атрибут:

[assembly: 
  RSharp.Rules.VisitorRule(
    VisitorInterfaceName = "IFigureVisitor",
    HierarchyBaseType    = "IFigure"
    )
]

После обработки проекта компилятором R# (rsc.exe) в интерфейс IFigureVisitor и во все классы, реализующие IFigure, будут добавлены все необходимые методы.

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

Чуть ниже я расскажу, как реализовать это метаправило. А пока давайте разберемся, что же такое метаправила и как работает компилятор R#.

Компилятор R#

Сначала терминология

Метаправило – это некое действие, выполняемое во время компиляции, цель которого – та или иная модификация AST обрабатываемой программы. Метаправила оформляются в виде отдельных классов, называемых метаклассами, которые, в свою очередь, помещаются в метасборки или основной проект (здесь и далее, если речь идет просто о проекте, то это проект, в котором нужно производить трансформацию кода, т.е. обычный проект или проект R#), помеченные атрибутом MetaRule. Класс метаправил обязан реализовать интерфейс RSharp.Meta.IMetaRule:

public interface IMetaRule
{
  void Initialize(
    RProject project, 
    RProject metaProject, 
    Parser parser,
    CompilerErrorCollection errors);

  void Start();
}

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

Метасборка – это внешняя сборка, содержащая метаклассы. Она оформляется в виде отдельного C#-проекта. Перед использованием метасборки должны быть зарегистрированы в проекте. Для этого в одном из файлов проекта, расположенном в папке RSharp, нужно указать глобальный атрибут RegisterMetaAssembly, в параметре которого нужно передать путь к подключаемой метасборке:

[assembly: RegisterMetaAssembly(
  @"..\..\..\RSharp.Rules\bin\Debug\RSharp.Rules.dll")
]

Информация о метасборках должна находиться в файле, расположенном в подпапке RSharp проекта.

Метакласс – это класс, реализующий метаправило, или вспомогательный класс, используемый метаправилами. Метаклассы могут быть расположены в метасборках или в коде проекта. В последнем случае они обязаны быть помечены атрибутами MetaRule или MetaCode.

Метакласс должен находиться в файлах, расположенных в подпапке RSharp проекта.

Основной проект (трансформируемый проект) – проект C#, в котором используется трансформация, производимая средствами R#. В дальнейшем под словом "проект" без уточнений понимается основной проект.

Метапроект – набор файлов внутри основного проекта, располагающихся в подкаталоге RSharp. Все файлы, находящиеся в этом подкаталоге, выделяются из AST основного проекта и помещаются в отдельное AST метапроекта.

ПРИМЕЧАНИЕ

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

Создание проекта, содержащего метаправило

Чтобы использовать R# в своем проекте, нужно:

1. Создать проект метасборки (обычный C#-проект). В нем нужно обязательно добавить ссылки (references) на сборки RSharp.Compiler, RSharp.Query и RSParser. Также желательно не удалять ссылку на System.Xml.

2. Создать в нем класс и реализовать в этом классе интерфейс RSharp.Meta.IMetaRule.

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

2.2. В методе IMetaRule.Start произвести трансформацию AST проекта, переданного в качестве параметров при вызове метода IMetaRule.Initialize. Последовательность вызовов такова: сначала вызывается Initialize, затем устанавливаются значения публичных свойств, а затем вызывается Start.

3. Зарегистрировать метасборку в основном проекте с помощью атрибута RegisterMetaAssembly (см. выше).

4. Описать вызовы метаправил. Для этого в метапроекте размещаются описания глобальных атрибутов, полные (полностью квалифицированные) имена которых совпадают с полными именами метаклассов. При этом через параметры атрибутов можно задать параметры метаправил. Например, для вызова метаправила RSharp.Rules.TempRule нужно описать следующий атрибут:

[assembly: RSharp.Rules.TempRule()]

а для описания правила RSharp.Rules.VisitorRule, имеющего параметры VisitorInterfaceName и HierarchyBaseType:

[assembly: 
  RSharp.Rules.VisitorRule(
    VisitorInterfaceName = "IFigureVisitor",
    HierarchyBaseType    = "IFigure"
    )
]

Эти атрибуты могут быть описаны в любом файле метапроекта. Причем они будут удалены из проекта перед генерацией исходного C#-кода.

5. Вызвать утилиту rsc.exe, которой в качестве параметра передать путь к VS-проекту, код которого нужно трансформировать:

rsc.exe -D:<путь к проекту>
ПРИМЕЧАНИЕ

В принципе, вместо утилиты командной строки можно создать объект-компилятор и выполнить все действия программно. rsc.exe – это всего лишь обертка над этим программным компилятором. Вызов rsc можно поставить в шаг prebuild VS .NET.

Собственно, все. rsc.exe и производит все действия. После его запуска (если не возникло ошибок в процессе его работы) в папку .RSharp помещается трансформированный код основного проекта. Точнее, код проекта помещается в подпапку .RSharp\Generated. В подпапке .RSharp\MetaCode можно найти метакод, сгенерированный компилятором R#, и метасборку MetaAssembly.dll. В MetaAssembly.dll компилятор R# помещает весь метакод и дополнительный сервисный код, вызывающий метаправила из сгенерированного метакода и внешних метасборок. Это позволяет компилятору R# избавиться от интерпретации, тем самым обеспечивая более высокую скорость работы.

Что же происходит при запуске rsc.exe

Собственно, компилятор R# делает следующее:

  1. Производит синтаксический разбор кода всего проекта, превращая его в AST.
  2. Ищет описание внешних метасборок (атрибуты RegisterMetaAssembly) и загружает все эти сборки.
  3. Ищет классы, помеченные атрибутами MetaRule или MetaCode, вычленяет их из кода проекта и помещает в отдельные файлы.
  4. Создает загрузочный код и компилирует его вместе с кодом, полученным на шаге 3, в специальную метасборку (MetaAssembly.dll).
  5. Передает управление метасборке. Метасборка получает ссылку на AST проекта и метапроекта (полученного на стадии 3), после чего вызывает каждое метаправило (как из внешних метасборок, так и у динамически скомпилированной).
  6. Производит трансформацию AST проекта.
  7. Превращает трансформированное AST в код на C#, который можно откомпилировать обычным компилятором C#. Результат трансформации помещается в подкаталог с именем .RSharp\Generated. В этом подкаталоге создается иерархия, аналогичная иерархии основного проекта, за исключением файлов, входящих в метапроект (то есть файлов из подкаталога RSharp). Если в основном проекте файл был подключен по ссылке, он все равно будет скопирован.

При вызове метаправила происходит следующее:

  1. Загружается объект, его реализующий.
  2. У него вызывается метод IMetaRule.Initialize. В его параметрах метаправилу передается AST проекта, AST метапроекта, парсер (с помощью которого можно считать дополнительные файлы), объект, через который можно выдать диагностирующее сообщение или сообщение об ошибке.
  3. Значения свойств инициализируются значениями соответствующих именованных аргументов метаправила.
  4. Вызывается метод Start. В этом методе происходит трансформация.

Правила генерации кода для R#

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

Мало того, что код генераторов был далек от идеала, так он еще зависел от бинарного модуля RSParser.dll (т.е. парсера). Это приводило к необходимости повторной генерации кода после перекомпиляции. Иногда возникала ситуация, когда генератор требует нового формата метаданных, а сборки до его прогона содержат старый формат. Чтобы избавиться от всех этих проблем, было принято решение перевести генерацию кода для проекта R# на сам R#.

Генерация кода для проекта R# размещается в метасборке RSharp.Rules, собираемой одноименным проектом. Он находится в папке RSharp\RSharp.Rules\. Пока что работа над ним не завершена.

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

Синтаксис XPath-запросов

Синтаксис XPath можно узнать из MSDN, множества статей, посвященных этому, или на сайте http://www.w3.org/TR/xpath.

XPath – это язык запросов, позволяющий делать запросы по иерархическим структурам. Изначально он был рассчитан на поиск по XML, но концепция оказалась более широкой. В общем, стараниями Microsoft, Андрея Корявченко, Василия Воронкова и Павла Леонова, с помощью XPath стало возможным делать поиск по AST.

В простейшем случае XPath-запрос выглядит как путь к каталогу, только вместо каталогов выступает иерархия классов AST (классы начинающиеся с префикса R). "//" означают поиск по всем подкаталогам (вглубь по иерархии). Некоторые классы AST для упрощения написания XPath-запросов были переименованы с помощью атрибута NodeName. Например, класс RTypeClass, отображающий в AST определение класса, переименован в class:

[NodeName("class")]
public class RTypeClass : RTypeUdt
{
...

Это позволяет обращаться к нему по имени class. Стало быть, запрос:

//class

означает - найти все классы. А запрос из примера:

//class[rs:IsInheritedFrom('RSParser.CodeDom.AstNode')]

Означает найти все классы, унаследованные от типа RSParser.CodeDom.AstNode (это базовый класс для всех классов AST-модели).

rs:IsInheritedFrom – это вызов custom-функции, добавленной нами для упрощения поиска наследников некоторого типа.

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

Название класса Имя, используемое в запросе Описание
AstNode- Базовый класс для всех классов AST.
RAnonymousMethod- Анонимный метод.
RAnonymousMethodExpression- Выражение, содержащее анонимный метод.
RArglistExpression- Выражение «__arglist» используемое при задании аргументов.
RArglistParameterExpression- Выражение «__arglist» используемое при описании параметров.
RArrayCreateExpression- Выражение создания массива.
RArrayIndexerExpression- Выражение – доступ к элементам массива.
RArrayInitializerExpression- Выражение инициализатор массива.
RArrayTypeReference- Ссылка на тип массива.
RAssignmentExpression- Выражение – присвоение.
RAttributeArgumentattribute-argumentАргумент атрибута.
RAttributeDeclarationattributeАтрибут.
RAttributeSectionattribute-sectionСекция атрибутов.
RBaseReferenceExpression- Ссылка на базовый класс (base.xxx).
RBinaryOperatorExpression- Выражение – бинарный оператор (например оператор «==»).
RBlockStatement- Блок предложений (Statement).
RBreakStatement- Предложение «break».
RCastExpression- Приведение типов.
RCatchClause- «catch» из «try/catch».
RCheckedExpression- Выражение «checked(...)».
RCheckedStatement- Приведение «checked{ ... }».
RCompileUnitcompile-unitЕдиница компиляции (файл).
RConditionalExpression- Выражение «? :».
RConditionStatement- «if».
RConstructorctorКонструктор.
RConstructorInitializer- Конструкторный инициализатор. Например, «: base(...)» или «: this(...)».
RContinueStatement- Предложение «continue».
RDefaultValueExpression- Выражение «default(...)» (C# 2.0).
RDestructordсtorДеструктор.
RDirectionExpression- Модификаторы параметров «ref» и «out».
RDoWhileStatement- Предложение do{}while(...);
REmptyStatement- Пустое предложение.
REnumField- Поле перечисления.
RExpressionStatement- Предложение, состоящее из выражения.
RFixedStatement- Предложение «fixed { ... }»
RForeachStatement- Предложение «foreach».
RForInitializerExpressions- Первое (инициализирующее) выражение в цикле «for».
RForInitializerVarDecl- Переменные, объявляемые в цикле «for».
RForStatement- Цикл «for».
RGenericReferenceExpression- Ссылка на что-то.
RGotoCaseStatement- Предложение «goto case».
RGotoDefaultStatement- Предложение «goto default».
RGotoStatement- Предложение «goto».
RIndexerExpression- Выражение «base[...]».
RLabeledStatement- Метка (ответная часть оператора «goto»).
RLockStatement- Предложение «lock».
RMemberEvent- Абстрактный, базовый класс для описания события.
RMemberEventDecl- Декларация события.
RMemberEventImpl- Реализация события.
RMemberField- Поле.
RMemberMethodDecl- Декларация метода.
RMemberMethodImpl- Реализация метода.
RMemberPropertyDecl- Декларация свойства.
RMemberPropertyImpl- Реализация свойства.
RMemberReferenceExpression- Ссылка на некоторый член типа.
RMethodInvokeExpression- Выражение – вызов метода.
RNamespacenamespaceПространство имен.
RNamespaceImportnamespace-importИмпорт пространства имен "(«using»).
RNonArrayTypeReference- Ссылка на тип, не являющийся массивом.
RObjectCreateExpression- Выражение, создающее объект.
ROverloadableBinaryOperator- Перегрузка бинарного оператора.
ROverloadableTypeCastOperator- Перегрузка приведения типов.
ROverloadableUnaryOperator- Перегрузка унарного оператора.
RParameterDeclarationExpression- Декларация параметра.
RPointerReferenceExpression- Ссылка на указатель (unsafe-конструкция).
RPostDecrementExpression- Оператор «х—».
RPostIncrementExpression- Оператор «х++».
RPrimitiveExpression- Примитивное выражение: строковый литерал, число, true, false, null.
RProjectprojectПроект VS.
RPropertySetValueReferenceExpression- Параметр value доступный в set-ере свойста.
RResourceAcquisitionExpression- Выражение внутри предложения «using(некий ресурс) { ... }».
RResourceAcquisitionVarDecl- Определение переменной в предложении «using(некая переменная) { ... }».
RReturnStatement- Предложение «return».
RSizeOfExpression- Предложение «sizeof».
RStackAllocExpression- Предложение «stackalloc[...]».
RSwitchSection- Секция предложения «switch».
RSwitchStatement- Предложение «switch».
RThisReferenceExpression- Ссылка на «this».
RThrowExceptionStatement- Предложение «throw».
RTryCatchFinallyStatement- Предложение «try/catch/finally».
RTypeClassclassПриведение типов.
RTypeConstructor- Статический конструктор (конструктор типа).
RTypeDelegatedelegateОпределение делегата.
RTypeEnumerationenumОпределение перечисления.
RTypeInterfaceinterfaceОпределение интерфейса.
RTypeOfExpression- Выражение «typeof(тип)».
RTypeOrNamespaceAlias- Алиас пространства имен, или типа (алиас в «using»).
RTypeOrNamespaceName- Имя типа или пространства имен.
RTypeParameter- Параметр типа.
RTypeReferenceExpression- Ссылка на тип.
RTypeStructstructОпределение структуры.
RUnaryExpression- Унарное выражение.
RUncheckedExpression- Выражение «unchecked()».
RUncheckedStatement- Предложение «unchecked { ... }».
RUnresolvedReferenceExpression- Неразрешенная ссылка (ссылка на нечто, тип чего пока неизвестен).
RUnsafeStatement- Предложение «unsafe».
RUsingStatement- Предложение «using(...) { ... }».
RVariableDeclarationStatement- Описание переменной.
RVariableDeclarator- Описание переменной.
RVariableReferenceExpression- Ссылка на переменную.
RWhileStatement- Предложение «while».
RYieldReturnStatement- Предложение «yield return».
RYieldBreakStatementПредложение «yield break».

С помощью CodeAnalyzer можно загружать реальные проекты C# и тренироваться в написании XPath-запросов, или отлаживать создаваемые запросы. При навигации по дереву AST в этом проекте формируется полный путь к текущей ветке в формате XPath. Так что с помощью этого проекта можно значительно упростить написание запросов.

Запросы можно использовать в коде метаправил для упрощения локализации тех или иных ветвей AST. Запрос можно делать как по всему проекту, так и начиная с любой подветки. Естественно, что запросы по поддереву выполняются быстрее. Однако многие виды запросов, выполняемых по всему проекту, тоже выполняются довольно шустро. Этому способствует то, что при их выполнении процессор запросов отсекает ветки, которые явно недостижимы. Это стало возможно опять же благодаря генерации кода. В процессе генерации кода для процессора запросов строится матрица достижимости, которая и используется во время выполнения. Так, например, получение списка классов выполняется очень быстро за счет того, что классы не могут быть вложены в методы, стало быть, отсекается просмотр огромного числа ветвей, точно не содержащих искомые ветви.

Ниже приведены примеры использования API XPath-запросов. Первый запрос находит все классы-наследники класса RSParser.CodeDom.AstNode:

string query = "//class[rs:IsInheritedFrom('RSParser.CodeDom.AstNode')]";
AstNodeCollection cls = XPathProvider.QueryNodes(_project, query);

Второй запрос находит AST-ветку интерфейса ISomeInterface

query = "//interface[@Name = 'ISomeInterface']";
RTypeInterface itfDef;
XPathProvider.QueryNode<RTypeInterface>(_project, query, out itfDef);

Meta ID

При реализации метаправил часто встречается задача быстрого поиска тех или иных веток AST. Конечно, их можно искать с помощью XPath-запросов, но это довольно медленно, да и запросы писать надо. Метаидентификаторы позволяют пометить участок кода с помощью специальным образом оформленного #region-а, и впоследствии получить его AST одной строчкой кода.

Например, при описании метаправила, автоматизирующего реализацию паттерна Посетитель (описанной в разделе «Метаправило для реализации паттерна Посетитель (Visitor) на R#»), шаблон метода AcceptVisitor получается из отдельного файла, в котором он помечен метаидентификатором «AcceptVisitor»:

public class AstNodeTemplate
{
  #region Meta.ID(AcceptVisitor)
  public override void AcceptVisitor(IVisitor visitor)
  {
    visitor.Visit(this);
  }
  #endregion
  ...
}

Это позволяет в нужном участке кода получить AST этого метода следующей строкой кода:

RMemberMethodImpl acceptVisitor =
  (RMemberMethodImpl)temlateCompileUnit.MetaIdMap["AcceptVisitor"];

Как видите метаидентификатор – это строка, с которой ассоциирована некоторая ветка AST.

При разборе файла формируется ассоциативный массив метаидентификаторов, который доступен через свойство MetaIdMap класса RCompileUnit.

Контекстная замена текстовых полей в AST

При модификации AST часто появляется потребность изменить значение определенных свойств в некоторой подветке AST. Например, после загрузки шаблона метода AcceptVisitor у него нужно поменять значение типа параметра visitor с IVisitor на имя интерфейса Посетителя. Это можно сделать, найдя вручную ветку AST, отвечающую за хранение типа параметра, и изменив его. Однако при этом возникают две проблемы:

  1. Даже довольно простые конструкции в AST представляются в виде ветвистого дерева, и найти нужную ветку не так-то просто.
  2. Ссылки на нижележащие ветки в AST зачастую бывают обобщенными. Например, оператор if выражается в AST классом RConditionalExpression, имеющим три свойства: TrueExpr, FalseExpr и BoolExpr. Тип всех свойств RExpression. RExpression – это абстрактный базовый класс для всех выражений. В экземпляре класса RConditionalExpression в свойствах содержатся ссылки не на RExpression, а на его потомков (например, BoolExpr зачастую содержит ссылку на RBinaryOperatorExpression). Это приводит к тому, что часто приходиться производить приведение типов, что муторно и чревато ошибками.

Чтобы не возиться с длинными выражениями, перегруженными частыми приведениями типов в AST R# была добавлена возможность производить быструю контекстную замену строковых свойств. Это делается методом ReplaceSubNodesText. Он реализован у всех классов AST и принимает два строковых аргумента. Первый должен содержать искомую подстроку, а второй – строку, на которую нужно заменить подстроку. Так следующий пример заменит все вхождения подстроки "IVisitor" на имя интерфейса, содержащееся в переменной itfDef.Name.

acceptVisitor.ReplaceSubNodesText("IVisitor", itfDef.Name);

Так как строка "IVisitor" содержится только в ссылке на тип параметра метода AcceptVisitor, заменяется только он, а остальные строковые свойства остаются неизменными.

В приведенном примере только одно свойство в AST содержало значение "IVisitor", но это не значит, что нельзя производить множество замен за один раз. Таким образом, можно буквально парой строк кода реализовать аналог шаблонов C++. :)

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

Для более сложных замен можно использовать методы посещения, основанные на делегатах или паттерне Посетитель.

Глубокое клонирование

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

Работа с атрибутами

R# позволяет находить конструкции C#, помеченные атрибутами, а также находить и удалять атрибуты по их имени. Очень часто при реализации метаправил часть информации декларируется атрибутами в коде. Как ни смешно это звучит, удаление атрибутов, излишних в результирующем коде, оказалось востребованным.

Метаправило для реализации паттерна Посетитель (Visitor) на R#

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

[assembly: 
  RSharp.Rules.VisitorRule(
    VisitorInterfaceName = "IFigureVisitor",
    HierarchyBaseType    = "IFigure"
    )
]

где VisitorInterfaceName задает имя интерфейса, в который будут добавляться необходимые декларации метода Visit (по одному для каждого класса, входящего в посещаемое множество), а HierarchyBaseType – имя базового типа посещаемого множества.

Вот реализация этого метаправила:

using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.XPath;

using RSharp.Meta;
using RSParser.CodeDom;
using RSParser.Engine;
using RSharp.Query;

namespace RSharp.Rules
{
  public class VisitorRule : IMetaRule
  {
    /// <summary>
    /// Метод осуществляющий основную работу по трансформации.
    /// Он добавляет к интерфейсу Посетителя методы Visit для каждого
    /// не абстрактного класса, и реализацию метода AcceptVisitor в классы
    /// посещаемого множества.
    /// Метод можно использовать из других метаправил (например, для экономии 
    /// времени и повторного использования результатов запросов).
    /// </summary>
    /// <param name="itfDef">AST интерфейса Посетителя.</param>
    /// <param name="classes">Список AST классов иерархии.</param>
    /// <param name="temlateCompileUnit">
    /// CompileUnit, содержащий шаблоны методов Visit и AcceptVisitor.</param>
    public static void Go(
      RTypeInterface itfDef,
      RTypeClass[] classes,
      RCompileUnit temlateCompileUnit)
    {
      // Список членов интерфейса, в которые необходимо добавить методы Visit.
      RTypeMemberCollection itfMembers = itfDef.Members;

      // Получаем шаблон метода AcceptVisitor.
      RMemberMethodImpl acceptVisitor =
        (RMemberMethodImpl)temlateCompileUnit.MetaIdMap["AcceptVisitor"];
      acceptVisitor.ReplaceSubNodesText("IVisitor", itfDef.Name);

      // Получаем шаблон метода Visit.
      RMemberMethodDecl visit =
        (RMemberMethodDecl)temlateCompileUnit.MetaIdMap["Visit"];

      // На всякий случай удаляем имеющиеся члены интерфейса.
      itfMembers.Clear();

      // Перебираем все классы посещаемого множества.
      foreach (RTypeClass cls in classes)
      {
        // Абстрактные классы посетить невозможно.
        if (cls.IsAllModifiersSet(RModifier.Abstract)) 
          continue;

        // Добавляем реализацию метода AcceptVisitor к классу 
        // посещаемого множества.
        cls.Members.Add(acceptVisitor); 

        // Добавляем декларацию метода Visit,
        // соответствующего обрабатываемому классу.

        RMemberMethodDecl visitClone = (RMemberMethodDecl)visit.Clone();
        // Заменяем все вхождения TypeName во всех строковых свойствах на 
        // имя обрабатываемого класса.
        visitClone.ReplaceSubNodesText("TypeName", cls.Name);
        // Добавляем сформированный метод к списку членов интерфейса.
        itfMembers.Add(visitClone);
      }
    }

    #region IMetaRule Members

    public void Initialize(
      RProject project,                 // AST проекта.
      RProject metaProject,             // AST метапроекта.
      Parser parser,                    // Парсер.
      // Объект для вывода сообщений об ошибках.
      CompilerErrorCollection errors)
    {
      _project = project;
      _metaProject = metaProject;
      _parser = parser;
    }

    public void Start()
    {
      // Запрашиваем список классов-наследников класса с именем 
      // содержащимся в HierarchyBaseType.
      string query = "//class[rs:IsInheritedFrom('" + HierarchyBaseType + "')]";
      AstNodeCollection clss = XPathProvider.QueryNodes(_project, query);

      // Находим интефейс Посетителя.
      query = "//interface[@Name = '" + VisitorInterfaceName + "']";
      RTypeInterface itfDef;
      XPathProvider.QueryNode<RTypeInterface>(_project, query, out itfDef);

      // Трансформируем классы и интерфейс
      Go(
        itfDef,
        clss.ToArray<RTypeClass>(), 
        // GetTemlateCompileUnit возвращает CompileUnit,
        // содержащий шаблоны методов.
        Utils.GetTemlateCompileUnit(_parser));
    }

    private RProject _project;     // AST проекта.
    private RProject _metaProject; // AST метапроекта.
    private Parser _parser;        // Парсер.

    private string _visitorInterfaceName;

    /// <summary>
    /// Имя интерфейса, в который должны быть добавлены методы Visit
    /// </summary>
    public string VisitorInterfaceName
    {
      get { return _visitorInterfaceName; }
      set { _visitorInterfaceName = value; }
    }

    string _hierarchyBaseType;

    /// <summary>
    /// Класс, являющийся корнем иерархии объектов, которые должны посещаться.
    /// </summary>
    public string HierarchyBaseType
    {
      get { return _hierarchyBaseType; }
      set { _hierarchyBaseType = value; }
    } 

    #endregion
  }
}

А это класс, содержащий шаблонный код:

public class AstNodeTemplate
{
  #region Meta.ID(AcceptVisitor)
  public override void AcceptVisitor(IVisitor visitor)
  {
    visitor.Visit(this);
  }
  #endregion

  #region Meta.ID(Visit)
  void Visit(TypeName val);
  #endregion
...
}

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

Вся основная функциональность по модификации AST проекта находится в методе Go. Весь остальной код метаправил связан с реализацией кода, необходимого для взаимодействия с компилятором R#. Работа этого правила достаточно подробно описана в комментариях в коде.

Список проектов

На сегодня в состав R# входят следующие основные проекты:

RSParser – парсер C# 2.0. Предоставляет набор классов AST, классы парсера и лексера, класс для преобразования AST в C#-код. Если говорить об этом проекте в двух словах, то он позволяет читать C#-код в AST, просматривать и модифицировать AST (вручную) и генерировать по нему C#-код.

PreprocessorParser – препроцессор C#/R#. Это всего лишь вспомогательный проект, используемый проектом RSParser на этапе парсинга. Отдельной ценности не имеет, и возможно, в будущем будет объединен с проектом RSParser.

RSharp.Query – процессор XPath-запросов по AST.

RSharp.Compiler – компилятор R#. Пока что занимается только трансформацией исходного кода C#/R#-проектов и на выходе дает C#-код. В будущем, возможно, будет генерировать CLI-совместимые сборки. Компилятор R# доступен в виде компонента. Если есть желание использовать его из командной строки, то можно воспользоваться rsc.exe, который является консольной оберткой над компонентом компилятора.

rsc – консольная обертка над RSharp.Compiler.

CodeAnalyzer – графическая среда, позволяющая загружать отдельные C#-совместимые файлы или проекты Visual Studio (версий 2003, 2005 и 2005 Express), и наблюдать полученный AST в графическом виде (см. рисунок 1). Кроме всего прочего позволяет просматривать список метаидентификаторов, формировать и отлаживать XPath-запросы, генерировать C#-код или XML для заданных AST-веток. С помощью этого проекта проводится большая часть отладки парсера и многие научные эксперименты. :)

RSharp.Rules – этот проект содержит метаправила, используемые для генерации исходного кода самого R#. В данный момент он еще не закончен, но уже может являться примером реального применения R#.

Перечисленные выше проекты – это основные проекты R#. Но в составе R# есть и ряд мене значимых проектов. Вот их описание:

CocoR – модификация генератора парсеров Coco/R. Эта версия отличается от оригинала тем, что она обучена пониманию Unicode, в ней устранены ряд мелких недочетов и ошибок, а формат выводимой ею информации изменен так, чтобы его понимала VS IDE.

RSharp.Development.CodeGen – генератор кода. Этот проект был создан во времена, когда на самом R# еще не было возможности генерировать код. Так как R# содержит довольно объемную объектную модель (порядка 160 классов AST), то для него самого потребовалась создать немало генераторов кода. Первые генераторы были реализованы в составе обычного консольного приложения C#, и генерировали код методом банальной конкатенации строк. В качестве исходной информации использовались метаданные сборки RSParser, получаемые через рефлексию. После того как проект RSharp.Rules будет закончен, надобность в этом проекте отпадет.

TreeGrid3 – версия контрола TreeGrid из состава проекта Янус, адаптированная к .NET Framework 2.0. В ней C++-часть заменена наследованием от control-а System.Windows.Forms.ListView, используемого в виртуальном режиме. Его можно взять здесь или с SVN svn://rsdn.ru/TreeGrid.


Рисунок 1. GUR R#-а – проект CodeAnalyzer.

Как подключиться к проекту?

Если вы хотите просто посмотреть исходный код или повозиться с R#, то можно или скачать исходные коды в виде архива, или подключиться к системе контроля версий subversion (об этом в следующем разделе).

Если вы захотите поучаствовать в проекте, предложив новые идеи, то излагайте их на форуме проекта: http://rsdn.ru/forum/?group=prj.rsharp.

Если же вы готовы помочь проекту непосредственно своим трудом, то пришлите письмо с запросом на получение логина к базе subversion мне на e-mail vc@rsdn.ru.

Как скачать архив с исходниками?

Архивы можно получить по адресам:

Как подключиться к базе subversion?

Чтобы подключиться к базе subversion нужно:

  1. Скачать и проинсталлировать клиента SVN. Взять можно тут http://tortoisesvn.tigris.org/download.html.
  2. Создать в удобном для себя месте каталог RSharp, открыть его в Explorer или Total Commander, из контекстного меню выбрать Checkout и в поле "URL of repository" ввести svn://rsdn.ru/RSharp.
  3. Создать рядом с каталогом RSharp каталог TreeGrid, открыть его в Explorer или Total Commander, из контекстного меню выбрать Checkout и в поле "URL of repository" ввести svn://rsdn.ru/TreeGrid.

Как скомпилировать проект?

Независимо от того, скачали ли вы исходные тексты в архивах или получили их через систему контроля версий, вы должны создать корневой каталог, в который поместить каталоги RSharp с исходными текстами R# и каталог TreeGrid с исходными текстами TreeGrid. Другими словами, каталоги TreeGrid и RSharp должны находиться в одном каталоге.

Если вы все сделали верно, вам остается только скомпилировать проект. Для этого нужно открыть файл RSharp\RSharp.sln в VS или C# Express, и откомпилировать решение, или воспользоваться утилитой msbuild (если у вас нет VS 2005). Чтобы скомпилировать проект R# с помощью msbuild, нужно зайти в папку хххххх и выполнить в командной строке:

%SystemRoot%\Microsoft.NET\Framework\v2.0.40607\MSBuild.exe RSharp.sln.

Опции msbuild можно узнать, запустив эту утилиту с ключем /?. Более полную информацию о msbuild можно узнать по адресу http://winfx.msdn.microsoft.com/?//winfx.msdn.microsoft.com/winfx/ndp/dv_build/093395e1-70da-4f74-b34d-046c5e2b32e8.aspx.

Что требуется для компиляции?

Чтобы скомпилировать R#, понадобится .NET Framework 2.0 или VS 2005 (полноценная или версии C# Express), в состав которой также входит .NET Framework 2.0.

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

Если вы являетесь подписчиком MSDN, то можете скачать полноценную версию VS 2005 с сайта MSDN или заказать отдельным диском. В общем, если такая возможность есть, выбирайте именно полноценную VS 2005. Если же вы не являетесь подписчиками MSDN или не обладаете каналом, способным пропустить сотни мегабайт, то скачайте C# Express. Он доступен по адресу http://download.microsoft.com/download/8/c/7/8c784f26-8e95-43b9-90b9-56b511220dfb/vcssetup.exe. Это официальная публичная beta1. На момент выхода статьи была доступна также Post-beta1, о которой можно прочесть здесь http://blogs.msdn.com/astebner/archive/2004/10/19/244778.aspx, но очень советую не бросаться ее скачивать. Она очень нестабильна. По всей видимости, до выхода публичной beta2 (который должен состояться весной 2005 года), лучше не скачивать более свежих версий. Публичная beta1 – это относительно стабильная версия, которой более чем достаточно для изучения и работы с R#. На этой странице http://rsdn.ru/Forum/Message.aspx?mid=749633&only=1 вы можете найти полный список продуктов входящих в состав Express beta1 (в том числе сокращенных MSDN).

Если вы не хотите качать и C# Express, или если у вас слабый канал, как минимальный вариант можно скачать .NET Framework 2.0 по адресу http://download.microsoft.com/download/1/2/7/127cf849-dfd6-4f0f-b0e5-786eec3cdcfa/dotnetfx.exe.

Это более свежая версия .NET Framework, чем та, что входит в beta1. Я не тестировал R# на ней, но никаких теоретических предпосылок к тому, что R# не будет компилироваться и работать с ней, нет. Есть, правда, сомнение, что VS 2005 beta1 будет с ней работать без сбоев, ну да если вы собрались ставить VS 2005 beta1, то используйте Framework из той же поставки.

Если вы являетесь подписчиком MSDN, то очень советую также скачать "Avalon" Community Technology Preview. В него входит более новая версия .NET Framework 2.0 (если у вас уже стоит VS 2005 beta1, но возможно, эту версию Framework лучше не ставить), сокращенная версия WinFx SDK (видимо, так будут именовать после выхода Longhorn то, что ранее называлось .NET SDK), и собственно Avalon (графическая подсистема нового поколения, основанная на управляемом API).

Зачем он вам может понадобиться? В состав WinFx SDK входит самая свежая версия документации по .NET Framework 2.0. Если вы не имеете возможности его скачать, что можно пользоваться онлайн-версией документации, которая доступна по адресу http://winfx.msdn.microsoft.com.

Лицензия R#

R# доступен свободно всем со следующими ограничениями:

Вы не имеете права выпускать код R# (полностью или частично) под лицензиями, ограничивающими его применение в проектах любого типа. Например, выпуская продукт, использующий коды или части R# под лицензией GNU, вы обязаны четко описать, что такие-то коды/части проекта использованы вами под лицензией R#, и лицензия GNU на них не распространяется.

Вы не имеете права продавать коды или части R# как отдельный продукт.

Вы не имеете права удалять из исходных файлов или частей R# никакой информации, касающейся прав на копирование (копирайта) и авторства.

Если вы включаете коды R# в состав коммерческого продукта, то использованные вами коды R# не должны превышать 10% общего объема кода этого продукта.

Если вы используете R# (даже частично), вы обязаны снабдить свой продукт информацией о том, что при его разработке были использованы коды или наработки из проекта R#, и дать ссылку на страницу проекта R# (http://.rsdn.ru/?article/rsharp/rsharp.xml). Если вы разрабатываете GUI-приложение, и оно содержит диалог About (О программе), то такое упоминание должно содержаться в этом диалоге. Если к продукту прилагается документация и/или файл с описанием, то упоминание об R# должно быть помещено в эти файлы. Причем это упоминание должно быть доступно так же явно, как информация об авторских правах на сам продукт, использующий код R# (без применения поиска или других ухищрений, скрывающих упоминание). Любой пользователь вашего приложения должен иметь возможность беспрепятственно увидеть это упоминание.

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

Если по каким бы то ни было причинам вы не можете выполнить эту лицензию, то откажитесь от распространения R# или его частей. Вы также можете обратиться с запросом по адресу vc@rsdn.ru и, возможно, вам будет предоставлена эксклюзивная лицензия. Эксклюзивная лицензия может быть предоставлена только в письменном виде.

ПРЕДУПРЕЖДЕНИЕ

Под лицензию R# не попадет модификация CocoR, хранящаяся в репозитории R#. CocoR распространяется под лицензией GNU. CocoR не является частью R#. Он используется для генерации кода парсера.

Планы

Планы – это то, чего у нас всегда было больше всего! Причем ими никогда не жалко делиться. :)

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

Далее, у R# очень слабенькая подсистема разрешения имен. Во-первых, она написана на скорую руку и может содержать серьезные упущения. А во-вторых, она неполноценна. На сегодня она позволяет разрешать только имена типов. Причем только типов, объявленных в проекте. Полноценная система будет разрешать все имена в проекте, даже если тип или переменная объявлены в других проектах. Это будет существенным шагом вперед и позволит увеличить возможности R#. Однако даже сегодняшней подсистемы разрешения имен достаточно, чтобы сгенерировать код для самого R#.

Далее планируется реализовать API, сходный с System.Reflection, но отталкивающийся исключительно от кода (не требующий компиляции). R# уже сейчас реализует многие возможности, аналогичные System.Reflection, но для полноценной реализации требуется полноценная подсистема разрешения имен.

Повышение уровня абстракции средств трансформации. Сейчас ведутся эксперименты по выбору оптимальной семантики трансформации AST. Однако идеал еще не найден. В итоге хочется получить максимально декларативный и в то же время гибкий метаязык, чтобы тратить на трансформацию как можно меньше усилий. Одна из идей – разработка некого набора метавыражений (например, в функциональном виде), который позволит встраивать код трансформации непосредственно в C#-код или в атрибуты, ассоциированные с членами типов.

Также планируется создать набор стандартных метаправил, решающих наиболее часто встречающихся задач. Например, совершенно точно будет создано метаправило для введения в C# mixin-ов.

Входит в планы создание на базе R# АОП-фреймворка с расширенными возможностями метапрограммирования. Реализация такого фреймворка на R# дает возможность сделать порождаемый код максимально эффективным (так как затраты на перехват будут отсутствовать). При этом средства метапрограммирования позволят значительно расширить возможности аспектов.

В более отдаленном плане возможно создание из R# полноценного компилятора, порождающего .NET-сборки, а также более тесная интеграция R# с VS.

Как уже было сказано, на базе R# можно создавать и другие решения, но этим уже пусть занимаются другие. R# им в руки. :) Например, R# можно использовать для построения оптимизаторов кода.

Если у вас появятся какие-либо мысли по поводу этих планов, или другие идеи, то не стесняйтесь выражать их на форуме проекта http://rsdn.ru/forum/?group=prj.rsharp.


Эта статья опубликована в журнале RSDN Magazine #5-2004. Информацию о журнале можно найти здесь
    Сообщений 4    Оценка 615        Оценить