Главная страница   /   4.1. Что такое CodeDOM (Метапрограммирование в .NET

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

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

Кевин Хазард

4.1. Что такое CodeDOM

CodeDOM – это довольно сложный набор классов, которые можно найти в пространствах имен System.CodeDom и System.CodeDom.Compiler. Большинство типов в этих пространствах имен можно найти в сборках mscorlib.dll и System.dll в Global Assembly Cache (GAC). Из этого понятно, насколько важен CodeDOM в .NET Framework Class Library (FCL).

Одна из причин того, что CodeDOM находится на таком видном месте, заключается в том, что он был в .NET Framework с самого начала. CodeDOM был выпущен с оригинальной версией .NET 1.0 в феврале 2002 года. Он не очень сильно эволюционировал в следующих двух релизах фреймворка: в основном для поддержки улучшенной модели делегатов и добавления дженериков.

Однако, с 2005 года CodeDOM фактически не менялся, отчасти потому, что CodeDOM уже достаточно богат, чтобы поддерживать многие виды сценариев метапрограммирования. Другой ключевой причиной является появление деревьев выражений.

Граф кода ≈ дереву выражений

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

Графы кода могут быть превращены в исполняемый IL, когда приходит время их использовать. Деревья выражений, введенные в версии .NET 3.0, тоже могут представлять код как данные. Они также могут быть построены программно или разобраны синтаксически из исходного кода и превращены в IL для исполнения. Графы кода и деревья выражений кажутся одинаковыми на первый взгляд. За кулисами, однако, реализация этих двух систем метапрограммирования довольно разнородная. По мере изучения CodeDOM в этой главе и деревьев выражений в главе 6 вы научитесь понимать различия и пользоваться ими.

Чтобы включить Language Integrated Query (LINQ) в CLR и основные .NET языки, необходимо нечто принципиально отличное от CodeDOM. CodeDOM обладает многими возможностями метапрограммирования, которые LINQ может использовать для создания выражений динамически, но он упакован таким образом, что его трудно использовать в наборе расширений языка. Для выражений LINQ Microsoft решил пойти другим путем.

На данный момент вы можете спросить себя, действительно ли стоит изучать CodeDOM. Не переживайте. CodeDOM стоит того, потому что он все еще может делать некоторые интересные вещи, которые не могут делать деревья выражений, например, генерировать совершенно новые типы. Даже после введения деревьев выражений из-за этого появились некоторые популярные инструменты Microsoft, которые используют CodeDOM для большей части своей работы с метапрограммированием. Например, T4 использует CodeDOM для создания классов из шаблонов и компиляции их в сборки. ASP.NET MVC Framework и Microsoft Entity Framework – две популярные системы, которые используют T4.

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

Организация и типы CodeDOM

Пространство имен System.CodeDOM организовано в иерархическую коллекцию классов. В корне находится класс с именем CodeObject, который только предоставляет словарь UserData. Каждый наследуемый объект в пространстве имен имеет словарь, который может быть использован для хранения некоторой информации. Интересно отметить, что типом словаря для свойства UserData является ListDictionary пространства имен System.Collections.Specialized, которое эффективно для списков, содержащих десять или меньше элементов. Если вы дадите много данных в список UserData в CodeObject, производительность будет страдать. Схема, показанная на рисунке 4-1, изображает базовый класс CodeObject и несколько его ключевых производных.

Рисунок 4-1: Базовый класс CodeDOM CodeObject и частичный список наследуемых типов

От CodeObject наследуются несколько интересных классов, которые представляют собой базовую объектную модель .NET программы. Тип CodeNamespace представляет ... ну, пространства имен в .NET. CodeNamespaceImport используется для импорта одного пространства имен в другое, так чтобы ссылки на типы, которые не полностью пригодны в полученном коде, могли быть найдены. Типы CodeStatement представляют операторов в .NET языке. Типы CodeExpression соответствуют логике, которая составляет операторы. Мы обсудим различие между операторами и выражениями в следующем разделе.

Большинство других имен типов, показанных на рисунке 4-1, говорят сами за себя. Они описывают остальные основные компоненты программы: комментарии, директивы, типы (интерфейсы, классы и структуры), члены (методы, поля и свойства), параметры и ссылки на другие объекты. Цель CodeCompileUnit не очень понятна. По большей части, вы можете думать о типе CodeCompileUnit типа как о концепции CodeDOM .NET сборки. Вы можете использовать CodeCompileUnit для компиляции сборки в память для немедленного использования или на диск для загрузки традиционным способом.

Давайте заглянем немного глубже в иерархию и посмотрим на некоторые из типов CodeExpression, изображенных на рисунке 4-2.

Рисунок 4-2: Базовый класс CodeExpression и частичный список наследуемых типов

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

Класс CodeExpression служит в качестве корня для всех типов выражений в CodeDOM. Рисунок 4-2 показывает некоторые из наиболее распространенных выражений, которые вы, вероятно, использовали в своих графах кода. Программные элементы, такие как аргументы, бинарные операторы (сложение, умножение, равенство и так далее), операции приведения типов и вызовы методов могут быть использованы для построения алгоритма в качестве данных одного выражения. Типы выражений с Reference в именах выступают в качестве ссылок на другие типы CodeDOM.

Эти ссылочные выражения не следует путать с типом CodeTypeReference, показанным на рисунке 4-1. CodeTypeReference, наследуемый от CodeObject, служит только в качестве заполнителя для .NET типов во время выполнения. Например, в CodeCatchClause, о котором вы узнаете чуть позже, тип пойманного исключением задается как CodeTypeReference. .NET CLR позволяет любому типу объекта быть выброшенным при исключении, поэтому тип, используемый в CodeCatchClause, не указывается в качестве System.Exception (как могли ожидать C# и Visual Basic программисты). Вместо этого, этот параметр имеет тип CodeTypeReference, поэтому он способен поддерживать языки, которые могут обрабатывать выброшенные и словленные типы, которые не наследуются от System.Exception.

Теперь вы готовы рассмотреть следующий уровень абстракции: операторы. Рисунок 4-3 показывает некоторые из наиболее распространенных операторов, определенных в CodeDOM и их связь с базовым классом CodeStatement.

Рисунок 4-3: Базовый класс CodeStatement и частичный список наследуемых типов

Как операторы и выражения подходят друг другу

Операторы используют выражения. При создании экземпляра объекта оператора CodeDOM необходимо указать выражение, которое будет использоваться для описания составных частей. Рассмотрим следующий оператор, написанный на C#:

R = fn(A + B) / C;

Один из способов визуализировать это заключается в наборе вложенных или сцепленных вызовов функций:

Assign(R, Divide(Invoke(fn, Add(A, B)), C))

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

С внешней стороны находится присвоение, которое вы могли бы написать, используя CodeAssignStatement, показанный на рисунке 4-3. Но вы не можете логически начать оттуда. Вместо этого, вы должны пойти в центр оператора, который является операцией сложения. Для этого вы должны использовать выражение известное как CodeBinaryOperationExpression. Ему вы передали бы объекты CodeVariableReferenceExpression для переменных А и В вместе с типом оператора для выполнения сложения. Перемещаясь наружу, следующим выражением является вызов функции fn с результатом выражения сложения. Для этой части вы используете CodeMethodInvokeExpression, передавая ссылку метода и операцию сложения. Далее идет другая бинарная операция для деления результата функции на переменную C. Наконец, после всей этой работы вы можете создать свой CodeAssignStatement со ссылкой на переменную R и результат выражения деления.

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

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