Главная страница   /   4.3. Добавление объектов в граф кода (Метапрограммирование в .NET

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

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

Кевин Хазард

4.3. Добавление объектов в граф кода

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

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

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

Что такое Jubjub?

В этом разделе мы будем рассматривать различные возможности CodeDOM, выстраивая класс Jubjub. Автор Кевин Хаззард выбрал это имя и некоторые другие для наших примеров из произведений 19-го века «Бармаглот» (Jabberwocky) и «Охота на Снарка» Льюиса Кэрролла. Обучающие инструменты с именами Jubjub, Mimsy, Vorpal и Wabe гарантированно не конфликтуют с другими концепциями, которые мы пытаемся выразить при нашем продвижении вперед. Их странность также делает их яркими и запоминающимися. Когда вы учитесь метапрограммированию, названия и классификации могут начать спотыкаться друг о друга в вашем уме. В этом суть метапрограммирования и одна из тех вещей, которые делают его сложным. Написание кода, который создает код, является рекурсивным опытом, вроде того, как стоять между двумя зеркалами. Выбор странных имен для объектов может помочь вам разобраться, что является реальным, а что является всего лишь отражением (рефлексией). Зазеркалье мы идем!

Создание пространства имен при помощи импортирования

Пространства имен в .NET являются несколько искусственной конструкцией. Они полезны при работе с потенциальными столкновениями между типами, давая вам способ добавлять уникальность именам. Они также удобны для организации больших коллекций типов в более мелкие группы со значимыми, часто иерархическими именами. Пространства имен удобны, но не являются строго обязательными. Большинство C# кода, который вы видите в настоящее время, начинается с набора деклараций using для импорта пространства имен для всего файла, а затем идет объявление пространства имен, которое содержит одно или несколько определений типа. Может быть, вы видели код, который вместо этого организован таким образом:

namespace Whatever
{
	using System;
	class Program
	{
		static void Main()
		{
			Console.WriteLine("Hi!");
		}
	}
}

Обратите внимание, что объявление using находится внутри объявления namespace. Такой стиль импорта чаще встречался, когда .NET был новым и создавал некоторые тонкие различия в процессе компиляции. С течением времени и по разным причинам многие разработчики решили размещать декларации импорта за пределами объявлений пространств имен. Используя CodeDOM при построении графов кода, вы часто будете создавать сначала CodeNamespace, а затем вставлять в него некоторые объекты CodeNamespaceImport, отражающие вид организации кода, показанный ранее:

CodeNamespace mimsyNamespace = new CodeNamespace("Mimsy");
mimsyNamespace.Imports.AddRange(new[]
{
	new CodeNamespaceImport("System"),
	new CodeNamespaceImport("System.Text"),
	new CodeNamespaceImport("System.Collections")
});

Приведенный фрагмент создает пространство имен в результирующем коде Mimsy с тремя импортируемыми элементами для пространств имен System, System.Text и System.Collections, объявленных внутри него. Обратите внимание, что свойство Imports пространства имен, которое относится к типу CodeNamespaceImportCollection, поддерживает функцию AddRange для добавления массива импортируемых элементов в пространство имен за раз. Есть также функция Add коллекции, которая позволяет, чтобы импортируемые элементы были добавлены по одному за раз.

Если бы вам нужно было генерировать C# код для пространства имен mimsyNamspace, он выглядел бы следующим образом:

namespace Mimsy
{
	using System;
	using System.Text;
	using System.Collections;
}

Как вы можете видеть, декларация using была вставлена внутрь объявления namespace, отражая способ, которым коллекция Imports определяется как свойство внутри CodeDOM типа CodeNamespace. Теперь, когда вы можете создать базовый контейнер для динамически генерируемой структуры программы, давайте добавим объявление типа.

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

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

Листинг 4-5: Вспомогательная функция GenerateCSharpCodeFromNamespace
static string GenerateCSharpCodeFromNamespace(CodeNamespace ns)
{
	CodeGeneratorOptions genOpts = new CodeGeneratorOptions
	{
		BracingStyle = "C",
		IndentString = " ",
		BlankLinesBetweenMembers = false
	};
	StringBuilder gennedCode = new StringBuilder();
	using (StringWriter sw = new StringWriter(gennedCode))
	{
		CodeDomProvider.CreateProvider("c#")
			.GenerateCodeFromNamespace(ns, sw, genOpts);
	}
	return gennedCode.ToString();
}

Это вспомогательная функция вызывает GenerateCodeFromNamespace для провайдера кода C# с некоторыми популярными опциями. Исходный код, который генерируется, направляется в StringBuilder при помощи StringWriter и возвращается вызывающему элементу в виде String.

Вы можете добавить тип в граф кода при помощи CodeTypeDeclaration. Следующие строки кода добавляют класс с именем Jubjub в граф кода:

CodeTypeDeclaration jubjubClass =
	new CodeTypeDeclaration("Jubjub")
{
	TypeAttributes = TypeAttributes.NotPublic
};

Установка TypeAttribute на NotPublic сделает так, что тип Jubjub будет помечен как класс internal в C#. Если вы хотите сделать класс public, вы можете использовать вместо этого Public TypeAttribute. Важно отметить, что в отличие от многих других типов, выделенных этой главе, перечисляемый тип TypeAttribute не находится ни в одном из пространств имен CodeDOM. Он определен в пространстве имен System.Reflection. Когда вы копнете глубже в CodeDOM, на поверхность выйдет больше ссылок на Reflection API.

Теперь вы готовы добавить поле членов в класс Jubjub. Это можно сделать, создав объект CodeMemberField, установив его имя и тип, а затем добавить его в коллекцию Members класса Jubjub:

CodeMemberField wabeCountFld =
	new CodeMemberField(typeof(int), "_wabeCount")
{
	Attributes = MemberAttributes.Private
};
jubjubClass.Members.Add(wabeCountFld);
mimsyNamespace.Types.Add(jubjubClass);

Обратите внимание, что после создания объекта поля членов и добавления его в Members, класс Jubjub также был добавлен в коллекцию Types пространства имен. Класс Jubjub был построен независимо от пространства имен, а затем вставлен в него. Некоторые другие DOM, которые вы, возможно, использовали, работают по-другому, предоставляя функции фабрики внутри каждого контейнера, которые создают, а затем прикрепляют дочерние объекты по иерархии. CodeDOM, однако, использует более свободную форму, отключая модель «создать-затем-присоединить».

Давайте посмотрим на код, который будет сгенерирован из того, что было построено до сих пор. Вызов функции GenerateCSharpCodeFromNamespace, показанной ранее, и передачи ссылки mimsyNamespace дает следующие результаты:

namespace Mimsy
{
	using System;
	using System.Text;
	using System.Collections;
	internal class Jubjub
	{
		private int _wabeCount;
	}
}

Добавление в класс конструктора

Наш граф кода теперь имеет пространство имен с импортируемыми элементами и внутренний класс с именем Jubjub, содержащий закрытое поле. Но чтобы продолжить работу, вам нужно добавить другие члены, такие как методы и свойства. Однако, перед тем, как добавить их, давайте добавим конструктор в класс Jubjub при помощи типа CodeConstructor:

CodeConstructor jubjubCtor = new CodeConstructor()
{
	Attributes = MemberAttributes.Public
};

Если используется MemberAttribute Public, этот объект конструктора будет помечен как Public в графе кода. Поставщик кода будет использовать эти метаданные для создания конструктора как public в исходном коде, который будет выработан. Другой подобный тип в CodeDOM, который называется CodeTypeConstructor, также может быть использован для добавления конструкторов в классы. Но такие конструкторы будут помечены как static в C# (или Shared в Visual Basic). Терминология здесь является важной. Слово Type, появляющееся в имени класса между Code и Constructor, обозначает, что создается конструктор для типа (класса), который будет выпущен, а не для экземпляров класса. Не все .NET языки поддерживают статические конструкторы, так что обратите внимание на ваш целевой язык, прежде чем попытаться сгенерировать их в код на определенном языке.

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

var jubjubCtorParam =
	new CodeParameterDeclarationExpression(
		typeof(int), "wabeCount");

Обратите внимание, что когда имена типов CodeDOM являются длинными, мы иногда используем ключевое слово var, чтобы сделать код более читабельным. Но мы делаем это не потому, что мы ленивые, а для того, чтобы улучшить понимание. Далее вам нужно добавить выражение параметра в конструктор. Чтобы сделать это, добавьте выражение в коллекцию Parameters объекта конструктора:

jubjubCtor.Parameters.Add(jubjubCtorParam);

Добавление операторов

Чтобы новый добавленный конструктор что-то делал, вам нужно добавить операторы в его коллекцию Statements. Все типы операторов в CodeDOM наследуются от базового класса с именем CodeStatement. Некоторые из наиболее распространенных типов операторов показаны на рисунке 4-3. Операторы используют выражения для ссылки на объекты и для предоставления других основных строительных блоков для программы. Выражения CodeDOM наследуются от базового класса CodeExpression. Некоторые из наиболее распространенных типов выражений показаны на рисунке 4-2. Вы можете быстро распознать классы операторов и выражений в CodeDOM, потому что их имена типов заканчиваются на Statement и Expression соответственно.

Чтобы присвоить параметр конструктора wabeCount полю членов _wabeCount, используйте следующие производные CodeStatement и CodeExpression:

  • CodeFieldReferenceExpression: чтобы сослаться на поле _wabeCount
  • CodeThisReferenceExpression: чтобы включить ссылку this
  • CodeArgumentReferenceExpression: чтобы сослаться на параметр wabeCount
  • CodeAssignStatement: чтобы выполнить присвоение

Давайте начнем с создания ссылочного выражения для поля _wabeCount и аргумента конструктора wabeCount. Использование одного типа для создания объектов и другого типа, чтобы ссылаться на них, является общим паттерном в CodeDOM. Например, целое число _wabeCount было определено как член класса Jubjub при помощи типа CodeMemberField, который является подклассом класса CodeTypeMember (рисунок 4-1). Но для ссылки на этот член в операторе мы используем CodeFieldReferenceExpression, который, как следует из его названия, наследуется от CodeExpression.

Создание явных ссылок this

Когда люди пишут код вручную, включая ненужные ссылки на параметр this (или параметр Me в VB), это иногда считается дурным тоном, потому что код загромождается. Но когда вы автоматизируете генерацию исходного кода, вы иногда хотите быть более точными и включаете ссылки this для безопасности. Кто знает, когда локальная переменная или параметр будут введены в будущем, что непреднамеренно скроет член класса по имени? Но если ваш автоматически сгенерированный код использует достаточно уникальные имена или предназначен для редактирования разработчиками, вы можете опустить ссылки this, так как это улучшает понимание. Вы можете сделать это, передавая null всякий раз, когда вы, возможно, включили CodeThisReferenceExpression в конструкцию выражения.

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

var refWabeCountFld =
	new CodeFieldReferenceExpression(
		new CodeThisReferenceExpression(), "_wabeCount");
var refWabeCountArg =
	new CodeArgumentReferenceExpression("wabeCount");
var assignWabeCount =
	new CodeAssignStatement(refWabeCountFld, refWabeCountArg);
jubjubCtor.Statements.Add(assignWabeCount);

После того как вы написали графы кода, ваш мозг будет в состоянии понять эту последовательность шагов более функционально, как:

jubjubCtor.Statements.Add(
	new CodeAssignStatement(
		new CodeFieldReferenceExpression(
			new CodeThisReferenceExpression (),
			"_wabeCount"),
		new CodeArgumentReferenceExpression(
			"wabeCount")));

Когда вы увидите код, написанный профессионалами в CodeDOM, он часто будет выглядеть более компактно и свободно. А пока давайте придерживаться пошаговой модели, пока вы не начнете хорошо понимать его. Если вы сгенерируете C# код для того, что мы показали ранее, вы можете быть удивлены. Класс Jubjub будет по-прежнему лишен операторов. Чего вам не хватает? Давайте рассмотрим все шаги по порядку. Вы:

  • Создали пространство имен
  • Добавили в пространство имен два импортируемых элемента
  • Создали класс
  • Добавили в класс поле членов
  • Присоединили класс к пространству имен
  • Создали конструктор
  • Создали выражение присвоения
  • Добавили выражение присвоения конструктору

Ага! Мы забыли добавить конструктор в коллекцию Members класса Jubjub! Наше упущение было преднамеренным, потому что мы хотели показать, что вам не обязательно нужно присоединять целые объекты к графу кода CodeDOM. Можно, например, прикрепить объект контейнер, например, CodeTypeDeclaration, к пространству имен, прежде чем добавлять в него новые члены. Это вполне приемлемо, даже если порядок вложений приведет к тому, что граф кода создаст невалидный объект.

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

Уважайте инструменты

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

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

jubjubClass.Members.Add(jubjubCtor);

Если вы сгенерируете код для этого примера, вы увидите на консоли C# код, показанный в следующем листинге.

Листинг 4-6: Сгенерированный графом кода класс с полем и конструктором
namespace Mimsy
{
	using System;
	using System.Text;
	using System.Collections;
	internal class Jubjub
	{
		private int _wabeCount;
		public Jubjub(int wabeCount)
		{
			this._wabeCount = wabeCount;
		}
	}
}

Добавление в класс свойства

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

Листинг 4-7: Добавление свойства в граф кода
CodeMemberProperty wabeCountProp =
	new CodeMemberProperty() {
	Attributes = MemberAttributes.Public
		| MemberAttributes.Final,
	Type = new CodeTypeReference(typeof(int)),
	Name = "WabeCount"
};
wabeCountProp.GetStatements.Add(
	new CodeMethodReturnStatement(refWabeCountFld));
jubjubClass.Members.Add(wabeCountProp);

Генерация кода из графа сейчас создает свойство, которое выглядит вот так в C#:

public int WabeCount
{
	get
	{
		return this._wabeCount;
	}
}

В листинге 4-7 было создан CodeMemberProperty, отмеченный атрибутами Public и Final, он является целочисленным типом и проименован WabeCount. Маркировка типа как Public достаточно хорошо знакома, но что обозначает Final? В CLR маркировка класса как final делает его не virtual. Вы видели ссылки на этот маркер метаданных в IL коде в главе 2. Если вы удалите атрибут Final из кода в листинге 4-7 и запустите его снова, свойство будет сгенерировано вот так:

public virtual int WabeCount
{
	get
	{
		return this._wabeCount;
	}
}

Методы и свойства членов сгенерированного CodeDOM типа по умолчанию помечены как virtual (или не Final). Если вы хотите, чтобы свойство WabeCount было не virtual, вам нужно явно отметить его как Final.

CodeDOM класс CodeTypeReference используется для обозначения типа свойства в виде целого числа. До сих пор вам были нужны только ссылки для встроенных .NET в типов для таких вещей, но что, если вам нужно ссылаться на тип в закрытой сборке? Вы можете загрузить тип в программу-генератор и использовать встроенную функцию typeof, как вы делали для целых чисел. Но класс CodeTypeReference имеет перегруженный конструктор, который позволяет передать строку. Это удобно, если вы не хотите загружать зависимости, которые понадобятся сгенерированному коду позже, во время генерации кода. Например, если у вас есть тип, называемый Vorpal, который вы хотите назначить созданному свойству, вы можете установить его свойство Type в графе кода с помощью литеральной строки, содержащей имя типа:

Type = new CodeTypeReference("Vorpal"),

В отличие от типов членов методов и конструкторов в CodeDOM, CodeMemberProperty, использованный в листинге 4-7, не имеет вообще свойства Statements. Вместо этого у него есть два свойства GetStatements и SetStatements, которые используются для определения тела сгенерированных get и set соответственно. Обратите внимание, что мы добавили оператор управления потоком типа CodeMethodReturnStatement к свойству GetStatements, которое использует ту же ссылку на поле _wabeCount, которую мы использовали ранее в операторах конструктора. В общем, как только вы создали ссылочный объект в CodeDOM, целесообразно использовать его снова и снова там, где этот ссылочный объект нужен в графе кода.