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

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

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

Кевин Хазард

4.4. Метапрограммирование с CodeDOM

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

Использование логики ветвления

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

wabeCountProp.SetStatements.Add(
	new CodeAssignStatement(
		refWabeCountFld,
		new CodePropertySetValueReferenceExpression()));

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

public int WabeCount
{
	get
	{
		return this._wabeCount;
	}
	set
	{
		this._wabeCount = value;
	}
}

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

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

var suppliedPropertyValue =
	new CodePropertySetValueReferenceExpression();
var zero = new CodePrimitiveExpression(0);

Имена suppliedPropertyValue и zero относятся к ключевому слову value в мутаторе и литеральному целочисленному значению 0, соответственно. Эти имена сделают дальнейший код более легким для чтения и понимания.

Интересно отметить, что есть определенный тип выражений CodeDOM для ссылки на value в сеттере свойстве, иногда называемом мутатор. Для нулевого значения, с которым необходимо провести сравнение, используется тип CodePrimitiveExpression. В CodeDOM нет конкретных классов для представления типов Стандартной системы типов .NET (CTS) (что есть в некоторых других ориентированных на выражения интерфейсах метапрограммирования). Всякий раз, когда вам нужно выразить литеральные значения в CodeDOM, CodePrimitiveExpression, как правило, срабатывает вполне хорошо.

Теперь, когда у вас есть способ для обращения к двум значениям, чтобы сравнить их, вам необходимо выполнить с ними операцию сравнения «менее-чем». Стандартные операторы, которые принимают два параметра, описаны в перечисляемом типе CodeBinaryOperatorType в CodeDOM. Эти операторы можно разделить на несколько логических групп.

  • Math: Сложение, вычитание, умножение, деление и модуль
  • Identity: IdentityInequality и IndentityEquality
  • Bitwise: BitwiseOr и BitwiseAnd
  • Boolean: BooleanOr и BooleanAnd
  • Rank: ValueEquality, LessThan, LessThanOrEqual, GreaterThan и GreaterThanOrEqual

Мы заинтересованы в использовании CodeBinaryOperatorType.LessThan, чтобы сравнить предоставленное значение свойства с нулем, поэтому создайте CodeBinaryOperatorExpression, которое делает это. Прочитайте его сверху вниз, чтобы получить представление о значении выражения:

var suppliedPropValIsLessThanZero =
	new CodeBinaryOperatorExpression(
		suppliedPropertyValue,
		CodeBinaryOperatorType.LessThan,
		zero);

Вы согласны, что описательные имена переменных для объекта ключевого слова value и литерального значения 0 делают предыдущий код более читаемым? Вы, в конечном счете, создаете оператор для SetStatements свойства, который выглядит вот так, когда представляется в виде C#:

if (value < 0)
{
	this._wabeCount = 0;
}
else
{
	this._wabeCount = value;
}

Важно понимать, что выражение suppliedPropValueIsLessThanZero представляет только булев тест всего выражения: часть, которая читается (value < 0). Чтобы создать части оператора if и else, вы должны использовать этот тест в CodeConditionStatement, как показано в следующем листинге.

Листинг 4-8: Создание конструкции if/else при помощи CodeConditionStatement
var testSuppliedPropValAndAssign =
	new CodeConditionStatement(
		suppliedPropValIsLessThanZero,
		new CodeStatement[]
		{
			new CodeAssignStatement(
			refWabeCountFld,
			zero)
		},
		new CodeStatement[]
		{
			new CodeAssignStatement(
			refWabeCountFld,
			suppliedPropertyValue)
		});
wabeCountProp.SetStatements.Add(
	testSuppliedPropValAndAssign);

Выражение бинарного оператора suppliedPropValueIsLessThanZero, показанное выше, используется в качестве первого параметра при построении условия. Тест (value < 0) будет следовать if в коде, который генерируется. Два других параметра являются группами операторов, которые станут блоками, следующими части if и части else в результирующем коде. Существует перегруженный вариант конструктора CodeConditionStatement, который принимает на один параметр меньше. Как вы уже догадались, вы можете использовать этот конструктор, когда нужно сгенерировать в коде условный оператор if, с которым вы не хотите связывать ответвление else, чтобы оно было определено в графе кода.

После добавления выражения условия как SetStatements для свойства WabeCount, сгенерированный код для мутатора свойства теперь становится следующим:

set
{
	if ((value < 0))
	{
		this._wabeCount = 0;
	}
	else
	{
		this._wabeCount = value;
	}
}

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

Остерегайтесь CodeSnippetExpression

Хотя тип CodeSnippetExpression пригождается время от времени, вы должны по большей части избегать его. Он работает так, что вставляет фрагмент литерального кода в сгенерированный код. Но если фрагмент кода, вставленный таким образом, использует, например, особенности C++, вы никогда не будете иметь возможность сгенерировать C# или VB код из графов, которые включают его. С другой стороны, если вы хотите использовать какую-нибудь возможность языка, для которой CodeDOM не имеет поддержки, и вы знаете, что целевой язык никогда не изменится, типы фрагментов кода могут предоставить полезную гибкость.

Обращение к членам

Для полного применения новой бизнес логики, используемой в мутаторе свойства WabeCount, необходимо пересмотреть конструктор Jubjub, созданный в начале этого примера, и исправить его. Помните, что конструктор напрямую присвоил значение аргумента закрытому полю членов _wabeCount. Что делать, если предоставленное значение меньше нуля? Это присвоение должно быть сделано через свойство WabeCount, так чтобы с запрещенными значениями можно было работать, сохраняя все ваши драгоценные объекты Jubjub в идеальном состоянии. Чтобы сделать это, определите CodePropertyReferenceExpression и используйте его вместо ссылки на поле, которая была использована для первоначального создания CodeAssignStatement, который будет служить телом конструктора. Вот ссылка на свойство и модифицированный оператор присваивания:

var refWabeCountProp =
	new CodePropertyReferenceExpression(
		new CodeThisReferenceExpression(),
		"WabeCount");
var assignWabeCount =
	new CodeAssignStatement(
		refWabeCountProp, refWabeCountArg);

Генерация кода для примера на данном этапе дает красиво отформатированный и высоко функциональный класс Jubjub, содержащий закрытое, экземплярное целочисленное поле членов, свойство для доступа к полю, которое обеспечивает простую логику проверки, и конструктор, который позволяет безопасно создавать экземпляр типа, вызывая сеттер свойства. Этот класс приведен в листинге 4-9.

Листинг 4-9: Более полный сгенерированный CodeDOM класс Jubjub
namespace Mimsy
{
	using System;
	using System.Text;
	using System.Collections;
	internal class Jubjub
	{
		private int _wabeCount;
		public Jubjub(int wabeCount)
		{
			this.WabeCount = wabeCount;
		}
		public int WabeCount
		{
			get
			{
				return this._wabeCount;
			}
			set
			{
				if ((value < 0))
				{
					this._wabeCount = 0;
				}
				else
				{
					this._wabeCount = value;
				}
			}
		}
	}
}

Вы можете найти исходный код для генератора кода Jubjub на этом этапе в примере кода для книги, как проект TypeDeclarations.

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

this._wabeCount = (value < 0) ? 0 : value;

Действительно, многие C++ и C# разработчики предпочитают этот синтаксис для простого тестирования языковых конструкций if/else и switch/case, которые являются немного громоздкими и потенциально снижают понимания читателя. К сожалению, CodeDOM не имеет типа выражений, который поддерживает простую логику «тест-и-разветвление-по-значениям», как эта. Интересно, однако, что деревья выражений, подробно описываемые в главе 6, очень хорошо поддерживают тернарные операции.

Вызов методов

Предположим, что вы хотели бы, чтобы класс Jubjub был разработан таким образом, чтобы иметь возможность отслеживать все значения, заданные вызовом мутатора свойства WabeCount. Чтобы сделать это, вам нужен некий вид массива для отслеживания измененных значений. Во-первых, вы создадите CodeTypeReference к ArrayList из пространства имен System.Collections. Затем вы создадите другое CodeMemberField и добавите его в коллекцию Members класса Jubjub:

var typrefArrayList =
	new CodeTypeReference("ArrayList");
CodeMemberField updatesFld =
	new CodeMemberField(typrefArrayList, "_updates");
jubjubClass.Members.Add(updatesFld);

Если вы сейчас сгенерировали код из mimsyNamespace, появится новая строка внутри определения класса Jubjub:

private ArrayList _updates;

Если бы вы построили CodeTypeReference, используя typeof(ArrayList) вместо string, новая строка была бы другой:

private System.Collections.ArrayList _updates;

Использование функции typeof является, безусловно, предпочтительным методом. Но даже если вы можете вставить операторы импорта в пространство имен CodeDOM, CodeDOM сам не имеет понятия импорта. Это автомат, поэтому он не нуждается в таких любезностях.

При использовании такого типа, как typeof(ArrayList), CodeDOM всегда будет генерировать его как полное имя типа в создаваемом исходном коде. Если вы ожидаете, что сгенерированный код будут читать люди, эти длинные имена могут снизить читабельность. Мы использовали строку (string), чтобы сделать его более дружелюбным глазу. Кроме того, вы уже вставлен импортируемый элемент в пространство имен графа кода для пространства имен System.Collections. Любой сгенерированный код, который ссылается на классы, определенные в этом пространстве имен (например, ArrayList) будут работать правильно и этим «неполным способом».

Далее, вам нужно обновить конструктор для создания экземпляра ArrayList. Для этого используйте CodeObjectCreateExpression, которое вы можете использовать всегда, когда вам нужно создать экземпляр класса. Думайте о нем как об операторе new для CodeDOM:

var refUpdatesFld =
	new CodeFieldReferenceExpression(
		new CodeThisReferenceExpression(), "_updates");
var newArrayList =
	new CodeObjectCreateExpression(typrefArrayList);
var assignUpdates =
	new CodeAssignStatement(
		refUpdatesFld, newArrayList);
jubjubCtor.Statements.Add(assignUpdates);

Перед созданием экземпляра ArrayList, создается CodeFieldReferenceExpression, которое относится к новому полю _updates. Оно используется здесь и снова на протяжении всего остального примера кода, показанного в этом разделе. CodeAssignStatement строится, чтобы выполнить присвоение, а затем добавляется к Statements конструктора класса Jubjub. Конструктор теперь выпускается как

public Jubjub(int wabeCount)
{
	this._updates = new ArrayList();
	this.WabeCount = wabeCount;
}

Обратите внимание, что мы намеренно вставили конструкцию ArrayList перед использованием мутатора свойства WabeCount. Сделано это было умышленно, потому что мутатор свойства будет обновлен моментально, чтобы добавить элемент в ArrayList всякий раз, когда меняется значение. Если ArrayList не был размещен в нужном месте, когда вы попытались добавить к нему элемент, будет выброшено NullReferenceException. Последовательность операторов в конструкторе важна. Чтобы изменить свойство WabeCount для выполнения обновления, вам нужно добавить CodeMethodInvokeExpression к его свойству SetStatements, вот так:

wabeCountProp.SetStatements.Add(
	new CodeMethodInvokeExpression(
		new CodeMethodReferenceExpression(
			refUpdatesFld,
			"Add"),
		refWabeCountFld));

Теперь мутатор для свойства WabeCount в классе Jubjub будет генерироваться следующим образом:

set
{
	if ((value < 0))
		this._wabeCount = 0;
	else
		this._wabeCount = value;
	this._updates.Add(this._wabeCount);
}

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

Листинг 4-10: Функция, которую вы хотите сгенерировать в классе Jubjub
public string GetWabeCountHistory()
{
	StringBuilder result = new StringBuilder();
	for (int ndx = 0; ndx < this._updates.Count; ndx++)
	{
		if ((ndx == 0))
			result.AppendFormat("{0}", this._updates[ndx]);
		else
			result.AppendFormat(", {0}", this._updates[ndx]);
	}
	return result.ToString();
}

Это довольно простая функция, выраженная в C#, но ее кодирование в графе кода CodeDOM потребует немного умственного упорства с вашей стороны. Мы будем идти шаг за шагом, чтобы помочь вам продумать все это. Для начала вам нужно создать CodeDOM объект для метода и добавить его к классу Jubjub. Помните, как вы использовали CodeMemberProperty ранее в этой главе, чтобы создать свойство WabeCount? Создание метода при помощи типа CodeMemberMethod проходит аналогично:

CodeMemberMethod methGetWabeCountHistory =
	new CodeMemberMethod
	{
		Attributes = MemberAttributes.Public
			| MemberAttributes.Final,
		Name = "GetWabeCountHistory",
		ReturnType = new CodeTypeReference(typeof(String))
	};
jubjubClass.Members.Add(methGetWabeCountHistory);

Новый метод, называемый GetWabeCountHistory, будет public и не-virtual и будет возвращать String. Мы также добавили его в класс Jubjub, чтобы убедиться, что вы не забудете сделать это позже. Помните: это нормально – добавлять метод в граф кода, даже если вы пока еще не добавили в него никаких операторов. Возвращаясь к листингу 4-10, следующими шагами будут создание экземпляра объекта StringBuilder и присвоение его ссылки локальной переменной с именем result. Вот как это делается:

methGetWabeCountHistory.Statements.Add(
	new CodeVariableDeclarationStatement(
		"StringBuilder", "result"));
var refResultVar =
	new CodeVariableReferenceExpression("result");
methGetWabeCountHistory.Statements.Add(
	new CodeAssignStatement(
		refResultVar,
		new CodeObjectCreateExpression(
			"StringBuilder")));

Этот блок кода начинается с использования CodeVariableDeclarationStatement для создания локальной переменной StringBuilder result. Затем создается ссылка на переменную result для использования здесь и далее, когда нужно вызывать для нее методы. Наконец, оператор присваивания добавляются новому свойству Statements метода для вызова оператора new для StringBuilder и присвоения ссылки переменной result.

Вновь обращаясь к листингу 4-10, следующее, что вам нужно сделать, это построить цикл for, чтобы перебрать каждый из элементов в _updates ArrayList и добавить отформатированные строки в созданный вами StringBuilder. Но C# синтаксис, показанный в листинге 4-10, не может быть создан именно так в графе кода CodeDOM. Компилятор C# предоставляет определенные частички синтаксического «сахара», которые делают код более читабельным. Одним из таких сладких удовольствий является возможность создания экземпляра локальной переменной, как переменной ndx, внутри выражения for, например:

for (int ndx = 0; ndx < this._updates.Count; ndx++)

В CodeDOM, однако, вы должны сконструировать это следующим образом:

int ndx;
for (ndx = 0; ndx < this._updates.Count; ndx++)

Перед созданием цикла for, давайте создадим локальную целочисленную переменную ndx и ссылку на нее. Переменная ndx используется несколько раз в цикле, приведенном в листинге 4-10. Наличие удобной ссылки сделает кодирование внутри цикла менее многословным:

methGetWabeCountHistory.Statements.Add(
	new CodeVariableDeclarationStatement(
	typeof(int), "ndx"));
var refNdxVar =
	new CodeVariableReferenceExpression("ndx");

Теперь вы готовы к созданию цикла for. Прежде чем вы посмотрите на код, который создаст и вставит эту конструкцию, выглядящую такой простой в C#, вы должны подумать о том, как построен цикл for. В нем есть следующие части: инициализация, проверка, инкремент и блок операторов. Тип CodeIterationStatement в CodeDOM принимает четыре параметра в конструктор. Они отлично подходят частям цикла for. Вы выполните простой CodeAssignStatement для инициализационной части конструктора, чтобы присвоить нулевое значение локальной переменной ndx:

new CodeAssignStatement(
	refNdxVar,
	new CodePrimitiveExpression(0))

Следующей частью является проверка, чтобы увидеть, должен ли быть выполнен блок цикла или нет. Вы можете сделать это с помощью CodeBinaryOperatorExpression типа LessThan, сравнивая ссылку на локальную переменную ndx со значением свойства Count для _updates ArrayList. Вы также будете использовать ссылку на поле _updates, которое вы построили раньше, чтобы сделать код немного более читабельным:

new CodeBinaryOperatorExpression(
	refNdxVar,
	CodeBinaryOperatorType.LessThan,
		new CodePropertyReferenceExpression(
		refUpdatesFld,
		"Count"))

Теперь вы готовы к обработке выражения инкремента. В CodeDOM нет способа, чтобы выразить оператор ndx++, как он показан в листинге 4-10, но вы можете записать его в виде ndx = ndx + 1. Вот как вы встроите его в граф кода. Вам понадобится для этого еще одно CodeBinaryOperatorExpression. Оно будет типа Add. Вам также понадобится другой CodeAssignStatement, чтобы обратно присвоить результат оператора сложения локальной переменной ndx:

new CodeAssignStatement(
	refNdxVar,
	new CodeBinaryOperatorExpression(
		refNdxVar,
		CodeBinaryOperatorType.Add,
		new CodePrimitiveExpression(1)))

Далее идет тело итератора for, и в верхней части графа находится оператор if/else, о котором вы узнали при построении сеттера для свойства WabeCount. Так же, как тогда, используйте CodeConditionStatement, чтобы выразить логику, показанную в листинге 4-10. Как вы помните, первой частью CodeConditionStatement является проверка. Вашей проверкой в данном случае является (ndx == 0), которое выглядит как другое CodeBinaryOperatorExpression. Оно будет типа ValueEquality, сравнивая ссылку на локальную переменную ndx с нулем:

new CodeBinaryOperatorExpression(
	refNdxVar,
	CodeBinaryOperatorType.ValueEquality,
	new CodePrimitiveExpression(0))

Блок, следующий if в листинге 4-10, – это вызов метода, поэтому закодируйте его в графе как CodeMethodInvokeExpression. Чтобы вставить это в массив CodeStatement, как требует CodeConditionStatement, необходимо обернуть его в CodeExpressionStatement. Это условие CodeDOM, которое должно быть выполнено, если требуются производные от CodeStatement:

new CodeExpressionStatement(
	new CodeMethodInvokeExpression(
		new CodeMethodReferenceExpression(
			refResultVar,
			"AppendFormat"),
		new CodePrimitiveExpression("{0}"),
		new CodeArrayIndexerExpression(
			refUpdatesFld,
			refNdxVar)))

Обратите внимание, как вы вызываете метод AppendFormat, используя ссылку на результирующую переменную, которую вы сохранили ранее, передавая аргумент строкового формата "{0}" и индексированное значение из _updates ArrayList по индексу, указанному ссылкой, локальной переменной ndx.

Блок else выглядит схожим образом, поэтому мы не будем показывать здесь CodeDOM код для него. Разница состоит лишь в том, что строка, переданная методу AppendFormat для StringBuilder, немного отличается. Тут как префиксы стоят запятые в выходных данных для второго и последующих элементов во время итерации, чтобы сделать выходные данные хорошо отформатированным, разделенным запятыми списком. Объединение всех блоков, касающиеся CodeIterationStatement, создает одно большое выражение, показанное в следующем листинге.

Листинг 4-11: Создание итератора при помощи CodeDOM
methGetWabeCountHistory.Statements.Add(
	new CodeIterationStatement(
		new CodeAssignStatement(
			refNdxVar,
			new CodePrimitiveExpression(0)),
		new CodeBinaryOperatorExpression(
			refNdxVar,
			CodeBinaryOperatorType.LessThan,
			new CodePropertyReferenceExpression(
				refUpdatesFld,
				"Count")),
		new CodeAssignStatement(
			refNdxVar,
			new CodeBinaryOperatorExpression (
				refNdxVar,
				CodeBinaryOperatorType.Add,
				new CodePrimitiveExpression(1))),
		new CodeConditionStatement(
			new CodeBinaryOperatorExpression(
				refNdxVar,
				CodeBinaryOperatorType.ValueEquality,
			new CodePrimitiveExpression(0)),
				new CodeStatement[] {
				new CodeExpressionStatement(
				new CodeMethodInvokeExpression(
				new CodeMethodReferenceExpression(
					refResultVar,
					"AppendFormat"),
				new CodePrimitiveExpression("{0}"),
				new CodeArrayIndexerExpression(
					refUpdatesFld,
					refNdxVar)))},
			new CodeStatement[] {
				new CodeExpressionStatement(
				new CodeMethodInvokeExpression(
					new CodeMethodReferenceExpression(
						refResultVar,
						"AppendFormat"),
					new CodePrimitiveExpression(", {0}"),
					new CodeArrayIndexerExpression(
						refUpdatesFld,
						refNdxVar)))})));

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

Последнее, что вы должны сделать, чтобы завершить метод GetWabeHistory, это вернуть значение. Вы можете сделать это путем создания CodeMethodReturnStatement, который вызывает метод ToString для ссылки на локальную переменную result, которую вы сохранили ранее:

methGetWabeCountHistory.Statements.Add(
	new CodeMethodReturnStatement(
		new CodeMethodInvokeExpression(
			new CodeMethodReferenceExpression(
				refResultVar, "ToString"))));

Полный пример кода для этого раздела можно найти в хранилище исходного кода книги в проекте под названием AddingAndInvokingMethods для главы 4. Выходные данные метапрограммы, показанной в следующем листинге, имеют метод GetWabeHistory, который точно соответствует цели, показанной в листинге 4-10.

Листинг 4-12: Класс Jubjub с его новым методом GetWabeHistory
namespace Mimsy
{
	using System;
	using System.Text;
	using System.Collections;
	public class Jubjub
	{
		private int _wabeCount;
		private ArrayList _updates;
		public Jubjub(int wabeCount)
		{
			this._updates = new ArrayList();
			this.WabeCount = wabeCount;
		}
		public int WabeCount
		{
			get
			{
				return this._wabeCount;
			}
			set
			{
				if ((value < 0))
				{
					this._wabeCount = 0;
				}
				else
				{
					this._wabeCount = value;
				}
				this._updates.Add(this._wabeCount);
			}
		}
		public string GetWabeCountHistory()
		{
			StringBuilder result;
			result = new StringBuilder();
			int ndx;
			for (ndx = 0; (ndx < this._updates.Count); ndx = (ndx + 1))
			{
				if ((ndx == 0))
				{
					result.AppendFormat("{0}", this._updates[ndx]);
				}
				else
				{
					result.AppendFormat(", {0}", this._updates[ndx]);
				}
			}
			return result.ToString();
		}
	}
}

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

Компиляция сборок

Прежде чем вы сможете сгенерировать сборку из пространства имен, необходимо поместить его в CodeCompileUnit. Этот класс имеет довольно странное имя и кажется еще более странным, учитывая, что процесс компиляции, который осуществляется через наследуемый тип CodeDomProvider, выполняется через метод с именем CompileAssemblyFromDom. Вы, возможно, ожидали, что метод будет называться примерно CompileAssemblyFromCompileUnit. Является ли CodeCompileUnit тем, что прародители CodeDOM в Microsoft считали высшим DOM типом для графа кода? Название, кажется, подразумевает это, но точно мы не знаем.

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

Листинг 4-13: Вспомогательная функция CompileNamespaceToAssembly
static Assembly CompileNamespaceToAssembly(
CodeNamespace ns)
{
	var ccu = new CodeCompileUnit();
	ccu.Namespaces.Add(ns);
	CompilerParameters cp =
	new CompilerParameters()
		{
			OutputAssembly = "dummy",
			GenerateInMemory = true
		};
	CompilerResults cr =
		CodeDomProvider.CreateProvider("c#")
		.CompileAssemblyFromDom(cp, ccu);
	return cr.CompiledAssembly;
}

Функция начинается с создания CodeCompileUnit и добавления переданного CodeNamespace в качестве параметра коллекции Namespaces. Затем строится объект CompilerParameters, для которого устанавливаются два свойства:

  • OutputAssembly: имя сборки в памяти или на диске
  • GenerateInMemory: индикатор, который указывает, должна ли быть сборка доступна немедленно в качестве объекта в памяти

OutputAssembly здесь дано имя "dummy", потому что нас не волнует, как она называется. Индикатор GenerateInMemory установлен на true, потому что нам не нужно, чтобы скомпилированная сборка была записана на диск. Вы собираетесь использовать ее сразу в работающем приложении. Поймите, однако, что компиляция в памяти может привести к утечке памяти в ваших программах в связи со способом, которым CodeDOM отмечает вновь скомпилированные сборки. Если вы загружаете сборки один раз в начале приложения и ожидаете, что они останутся в памяти, пока программа не завершится, это не должно быть проблемой. Но если вы создаете новые сборки снова и снова на протяжении всего жизненного цикла приложения, вы не должны использовать простой подход компиляции в памяти.

Наконец, вызывается метод CompileAssemblyFromDom для CSharpCodeProvider, передавая CompilerParameters и CodeCompileUnit в качестве параметров. Результатом является объект CompilerResults, который имеет свойство CompiledAssembly, ссылающееся на динамически скомпилированную сборку.

Есть несколько вещей, которые должна делать эта вспомогательная функция, но она не делает. Исключения, которые могут быть выброшены на протяжении всего процесса, не ловятся. И компилятор может столкнуться с ошибками в графе кода. Для вашего приложения стоит использовать метод CompileNamespaceToAssembly, показанный в листинге 4-13, в качестве отправной точки. Но вы должны добавить соответствующую обработку исключений и проанализировать коллекцию Errors для CompilerResults, прежде чем вернуться к вызывающему элементу.

Всегда устанавливайте OutputAssembly

Если вы не установили некоторое значение для свойства OutputAssembly CompilerParameters, компилятор выберет случайное имя сборки, которое гарантированно не будет конфликтовать с другими. Можно подумать, что поскольку вы генерируете сборку для использования в памяти, случайно выбранное имя сработает хорошо. Но если вы позволите компилятору выбрать случайное имя, вы не сможете использовать ссылку сборки в объекте CompilerResults, который возвращается. Всегда называйте OutputAssembly при вызове одного из методов Compile для поставщика кода CodeDOM.

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

Чтобы создать объект Mimsy.Jubjub из динамически скомпилированной сборки, мы будем использовать вспомогательную функцию с именем InstantiateDynamicType, показанную в следующем листинге. Она извлекает метаданные Type для названного класса и использует класс Activator, чтобы создать экземпляр указанного типа, передавая число переменных параметров конструктора. Метод CreateInstance класса Activator от Microsoft будет пытаться найти правильный конструктор в зависимости от типа и порядка параметров.

Листинг 4-14: Вспомогательная функция InstantiateDynamicType
static dynamic InstantiateDynamicType(Assembly asm,
	string typeName, params object[] ctorParams)
{
	Type targetType = asm.GetType(typeName);
	return Activator.CreateInstance(
		targetType, ctorParams);
}

Обратите также внимание, что метод InstantiateDynamicType возвращает тип dynamic C# 4.0. Как вы узнали в главе 1, не существует такой вещи, как тип dynamic в C#, хотя существование ключевого слова подразумевает обратное. За кулисами, объекты dynamic являются экземплярами System.Object, которые специальным образом обрабатываются компилятором. У них также есть специальный DynamicAttribute, применяемый к ним, чтобы позволить послекомпиляционному инструментарию продолжить процесс их обработки специальными, динамическими способами. Если вы хотите использовать код, показанный здесь, используя старый C# компилятор, измените ключевые слова dynamic на object. Тогда вы сможете выполнять собственную рефлексию к этим экземплярам object, чтобы вызывать методы и свойства динамически старомодным путем.

Со вспомогательными функциями, представленными в листингах 4-12 и 4-13, вы готовы к компиляции mimsyNamespace и выполнению класса Jubjub. Мы все это объединили как метод CompileAndExerciseJubjub, показанный в следующем листинге.

Листинг 4-15: Компиляция и создание экземпляров сгенерированных CodeDOM классов
static string CompileAndExerciseJubjub(
	CodeNamespace theNamespace, params int[] wabes)
{
	if (wabes == null || wabes.Length == 0)
		return string.Empty;
	Assembly compiledAssembly =
		CompileNamespaceToAssembly(theNamespace);
	dynamic bird = InstantiateDynamicType(
		compiledAssembly, "Mimsy.Jubjub",
		new object[] { wabes[0] });
	for (int ndx = 1; ndx < wabes.Length; ndx++)
		bird.WabeCount = wabes[ndx];
	return bird.GetWabeCountHistory();
}

Данный тестовый метод принимает CodeNamespace, который будет скомпилирован, и список wabes, чтобы передать его объекту Mimsy.Jubjub, для которого будет динамически создаваться экземпляр. Первый wabe передается конструктору, а все остальные устанавливаются через свойство WabeCount. Наконец, извлекается история всех ваших изменений WabeCount через метод GetWabeCountHistory, и она возвращается вызывающему элементу в виде строки.

Код для этого примера можно найти в хранилище исходного кода для книги как проект DynamicInvocation. В этом проекте вы также найдете метод, называемый CreateMimsyNamespace, который покрывает весь код, касающиеся создания графа кода Mimsy.Jubjub, в одном сжатом виде. Теперь вы можете вызвать тестовую функцию:

CodeNamespace mimsyNamespace =
	CreateMimsyNamespace();
Console.WriteLine(
	CompileAndExerciseJubjub(
		mimsyNamespace,
		8, 6, 7, 5, 3, -1, 9));

Выход на консоли будет выглядеть как то, что представлено на рисунке 4-4.

Рисунок 4-4: Результат динамического вызова динамически сгенерированного и динамически собранного класса

Вы можете видеть из сравнения выходных данных на рисунке 4-4 с кодом выше, что wabe -1, который был передан мутатору свойства WabeCount, было установлено значение 0.

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

InternalsVisibleToAttribute

При запуске код для проекта DynamicInvocation в главе 3 исходного кода примера он будет работать. Но если вы изменили код, а не использовали примеры, вы получите интересную ошибку. Помните начало примера Mimsy.Jubjub, когда вы отметили класс Jubjub при помощи MemberAttribute NonPublic? Это привело к тому, что класс Jubjub будут сгенерирован в качестве internal класс. При попытке создать экземпляр объекта из динамически загружаемой сборки, которая отмечена как internal, вы получите ошибку. Это имеет смысл, не так ли? Почему работающая сборка должна иметь доступ к internal классу в другой сборке? Она не должна. У вас есть выход, чтобы исправить эту проблему. Вы можете отметить класс как public. Или вы можете установить атрибут уровня сборки, известный как InternalsVisibleTo, для динамической сборки, полностью определив имя другой сборки, которая должна иметь доступ к типам, отмеченным как internal. Этот второй способ не является практичным, поэтому мы выбрали тот, где класс отмечается как public в исходном коде проекта DynamicInvocation.

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

В качестве последнего упражнения подумайте об изменениях, которые необходимы, чтобы класс Jubjub мог работать с другими типами данных, кроме целых чисел. Вот подсказка: это может быть сделано путем изменения двух строка кода из примера. С введением CodeTypeReference это изменение может быть сведено к одной строке. Видите ли вы связь между метапрограммированием и дженерик типами? Дженерики касаются многократного использования, и метапрограммирование предоставляет много возможностей для создания повторно используемых структур данных вашего собственного "производства".