Главная страница   /   6.3. Эффективное использование выражений (Метапрограммирование в .NET

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

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

Кевин Хазард

6.3. Эффективное использование выражений

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

Отладка выражений

Использование Expression API является более простым, более чистым опытом, чем попытка работать с IL. Тем не менее, каждый раз, когда разработчик пишет кусок кода, что-то может пойти не так. К счастью, есть несколько техник, которые можно использовать для отладки выражений. Давайте начнем с первого: визуализация выражений в отладчике.

Визуализация выражения в Visual Studio

Всякий раз, когда вы создаете выражение любого типа, вы можете получить структурную визуализацию этого узла в Visual Studio при запуске кода под отладчиком. Вы наводите указатель мыши на переменную в коде, переходите к опции Debug View и выбираете Text Visualizer. Рисунок 6-4 показывает, как выглядит выражение из раздела 6-1 в этом визуализаторе.

Рисунок 6-4: Визуализация выражения в Visual Studio. Язык может быть не похож на то, что вы видели до сих пор, но цель ясна.

Вы можете удивиться, почему язык, используемый в визуализаторе, не использует C# или VB. Выражения могут поддерживать опции, которые язык может быть не в состоянии выразить (например, блок fault), поэтому создатели визуализатора придумали другой язык, чтобы показать выражения. Ему не так трудно следовать, и это быстрый и простой способ понять, как выглядит ваше выражение в определенный момент его построения.

Примечание

Более подробную информацию о визуализаторе можно найти на http://mng.bz/E6Q7.

В следующем разделе вы увидите, как можно выйти на выражение во время выполнения.

Использование Reflection.Emit для отладки выражений

Хотя визуальное представление выражения – это хороший инструмент, иногда вы хотите, чтобы отладчик погрузился в код. К сожалению, выражение – это дерево, которое представляет структуру кода. Так ли это? При компиляции метода он выпускает для вас IL, как и код из главы 5, который использовал Reflection.Emit. Удивительно, что есть связь между выражениями и Reflection.Emit, которая позволяет создавать отладочную информацию для выражения. Давайте посмотрим, как это работает.

Аналогично примеру с обработчиком исключений, вы начнете с простого фрагмента кода "сложить два числа" из раздела 6.1.1. Первое, что нужно сделать, это создать группу динамических членов из Reflection.Emit API. Не волнуйтесь, вам не нужно писать никакого IL в целях отладки; эти члены выступают в качестве хоста для выражения, как вы сейчас увидите:

var name = Guid.NewGuid().ToString("N");
var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(
	new AssemblyName(name), AssemblyBuilderAccess.Run);
var module = assembly.DefineDynamicModule(name, true);
var type = module.DefineType(
	Guid.NewGuid().ToString("N"), TypeAttributes.Public);
var methodName = Guid.NewGuid().ToString("N");
var method = type.DefineMethod(methodName,
	MethodAttributes.Public | MethodAttributes.Static,
	typeof(int), new Type[] { typeof(int), typeof(int) });

Все, что делает этот код, это создает динамическую сборку в памяти вместе с динамическим типом и методом. Как вы видите, имена этих членов не имеет значения в данном случае; все, что действительно имеет значение – это поддержка отладки. Для этого вам нужен DebugInfoGenerator:

var generator = DebugInfoGenerator.CreatePdbGenerator();
var document = Expression.SymbolDocument("AddDebug.txt");

Генератор, созданный из CreatePdbGenerator(), будет использоваться, когда выражение скомпилируется. Кроме того, необходимо создать символьный документ на основе текстового файла. Файл AddDebug.txt в этом примере – это выражение, представленное на языке, используемом в визуализаторе выражений, который был показан в предыдущем разделе. Мы не будем показывать здесь код, но вы увидите, как выглядит его часть на рисунке в конце этого раздела.

Чтобы создать отладочные символы для разделов кода, вам нужно обернуть конкретный узел в дерево выражений при помощи DebugInfoExpression:

var addDebugInfo = Expression.DebugInfo(document,
	6, 9, 6, 22);
var add = Expression.Add(x, y);
var addBlock = Expression.Block(addDebugInfo, add);

Раздел данного документа, который преобразуется в обернутый узел выражения, определяется в вызове DebugInfo(). Этот объект DebugInfoExpression используется в вызове Block(), чтобы обернуть BinaryExpression, который представляет функциональность сложения этого выражения.

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

var lambda = Expression.Lambda(addBlock, x, y);
lambda.CompileToMethod(method, generator);
var bakedType = type.CreateType();
return (int)bakedType.GetMethod(methodName)
	.Invoke(null, new object[] { a, b });

В данном случае вы используете CompileToMethod(), чтобы указать, какой метод вы реализуете в динамической сборке вместе с отладочной информацией, связанной с этим методом. На данный момент, когда вы входите в вызов Invoke() в отладчике, вы входите в файл, указанный в объекте SymbolDocumentInfo. Рисунок 6-5 показывает, как это выглядит, когда вы входите в язык "выражения", скопированный в текстовый файл.

Рисунок 6-5: Отладка выражения. Даже если язык может выглядеть немного странно, все равно понятно, что вы остановились там, где в методе выполняется сложение.

Предостережение

Использование выражений для реализации методов в Reflection.Emit является хорошей альтернативой IL. Но в этом подходе есть один большой недостаток: вы можете использовать выражения только для определения статических методов. Вы не можете использовать выражение для метода экземпляра. Для получения дополнительной информации о том, почему так происходит, смотрите на http://mng.bz/U254.

Теперь вы знаете, как можно отлаживать выражения.

Давайте перейдем к другой теме: неизменности. Она, безусловно, влияет на дизайн, и это то, о чем говорится в следующем разделе.

Изменение деревьев выражений

Раздел 6.2 содержит обзор Expression API для создания динамических методов во время выполнения. В этом разделе вы посмотрите на выражения, неизменность и то, как вы можете создавать новые выражения, основанные на существующих, через класс ExpressionVisitor. Давайте сначала рассмотрим обоснования наличия неменяющихся деревьев.

Неизменность деревьев выражений

Чтобы начать разговор о неизменности выражений, давайте вернемся к выражению из раздела 6.1, которое складывало два целых числа:

Expression<Func<int, int, int>> add = (x, y) => x + y;

Допустим, кто-то захотел изменить это выражение, чтобы оно вычитало два аргумента, а не складывало их. Поскольку Expression<T> является ссылочным типом, выражение, на которые вы ссылались, теперь будет выполнять вычитание, а не сложение. Это не то, что вы хотите! Неизменяемые структуры данных хороши тем, что вы знаете, что как только структура создана, она не изменится. Это также имеет выгоду для перспективы параллелизма, потому что эти структуры автоматически потоково-безопасны.

Примечание

Для получения дополнительной информации о преимуществах (и некоторых недостатках) программирования с использованием неизменяемых значений смотрите http://en.wikipedia.org/wiki/Immutability и http://mng.bz/AId8.

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

Создание вариаций выражений

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

Ключевой класс, который вам нужен, это ExpressionVisitor. Как следует из названия, этот класс основан на паттерне Посетитель (Visitor), который предназначен, чтобы позволить вам пересечь сложные структуры объектов упрощенным способом, "посещая" конкретные методы, в которых вы заинтересованы.

Примечание

Больше информации о паттерне Посетитель можно найти на http://en.wikipedia.org/wiki/Visitor_pattern.

Вы даете подклассу ExpressionVisitor выражение, которое является вашей основой, а затем вы переписываете методы VisitXYZ(), в которых вы заинтересованы, чтобы создать новое выражение. Давайте создадим пользовательский посетитель, который изменит операцию сложения на операцию вычитания. Следующий код демонстрирует, что нужно, чтобы написать такой "посетитель".

Листинг 6-5: Создание "посетителя" выражения
internal sealed class AddToSubtractExpressionVisitor
	: ExpressionVisitor
{
	internal Expression Change(Expression expression)
	{
		return this.Visit(expression);
	}
	protected override Expression VisitBinary(BinaryExpression node)
	{
		return node.NodeType == ExpressionType.Add ?
			Expression.Subtract(
			this.Visit(node.Left), this.Visit(node.Right)) :
		node;
	}
}

Как вы можете видеть, не так много кода нужно для изменения выражения. В данном случае необходимо переопределить VisitBinary(), потому что вы пытаетесь найти математический узел выражения Add. Если вы найдете такой, то вы создаете новый узел, который вычитает дочерние узлы. Обратите внимание, что вы должны посетить узлы Left и Right с данного узла, потому что они также могут содержать операции сложения. Например, если вы не сделаете это, такое выражение

(x, y) => ((((32 * x) / 4) + y) + (x + 4))

станет таким

(x, y) => ((((32 * x) / 4) + y) - (x + 4))

Это не то, что вы хотите, потому что в выражении есть еще две операции сложения. Посещение дочерних узлов дает правильный результат:

(x, y) => ((((32 * x) / 4) - y) - (x - 4))

Примечание

До .NET 4.0 в .NET Framework не было способа посетить выражение. Класс ExpressionVisitor существовал в System.Linq.Expressions, но он был отмечен как внутренний (internal), так что вы не могли его использовать. Есть несколько способов поддерживать эту технику в .NET 3.5: смотрите http://mng.bz/09bP и http://mng.bz/a3N3 для информации об этих подходах.

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