Главная страница   /   9.2. Обзор инструментов (Метапрограммирование в .NET

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

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

Кевин Хазард

9.2. Обзор инструментов

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

Вот где в игру вступают инструменты. Они расширяют C#, чтобы вы имели возможностью выделить повторно используемые фрагменты кода и легко их использовали. Более того, вам также не нужно знать детали IL, чтобы сделать это. В данном разделе вы увидите, что Spring.NET и PostSharp могут сделать, чтобы вы могли использовать метапрограммирование в C#. И мы начнем со Spring.NET.

Что такое Spring.NET?

Первый инструмент, который мы рассмотрим, называется Spring.NET (www.springframework.net). Spring.NET – это интересная коллекция фреймворков и инструментов, таких как Spring.Data и Spring.Messaging, но эта глава посвящена компоненту Spring.Aop. Данная сборка предоставляет вам возможность «вплести» код в класс во время выполнения. Это делается путем создания динамического прокси интерфейса во время выполнения. Рисунок 9-2 иллюстрирует то, что делает Spring, чтобы добавить аспекты в ваш код.

Рисунок 9-2: Spring создает обертку вокруг объекта, который вы хотите использовать. Эта обертка добавляет к методам средства, которые являются понятными для вызывающего и вызываемого элементов.

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

Примечание

Spring.Aop доступен в NuGet на www.nuget.org/packages/Spring.Aop.

Перехват использования свойства при помощи Spring.NET

Чтобы вы поняли, какие возможности вам может предложить Spring.Aop, вы создадите перехватчик, который вы можете использовать, чтобы узнать, когда используется свойство. Это не потребует много работы – узнать, когда используется свойство, при помощи Spring: вам нужно создать класс, реализующий IMethodInterceptor. Следующий код показывает, как это можно сделать.

Листинг 9-4: Использование IMethodInterception для свойства
public sealed class PropertyInterceptor
	: IMethodInterceptor
{
	private static bool IsPropertyMethod(MethodBase method)
	{
		return (from property in method.DeclaringType.GetProperties(
				BindingFlags.Public | BindingFlags.Instance)
			where (property.GetGetMethod() == method ||
				property.GetSetMethod() == method)
			select property).Any();
	}
	public object Invoke(IMethodInvocation invocation)
	{
		if (PropertyInterceptor.IsPropertyMethod(invocation.Method))
		{
			Console.Out.WriteLine(
				"Property {0} was invoked.",
				invocation.Method.Name);
		}
		return invocation.Proceed();
	}
}

Единственный метод в IMethodInterception, который вам нужно реализовать, это Invoke(). Вы используете свойство Method для аргумента, чтобы определить, является ли метод тем, что используется в качестве геттера или сеттера для свойства. Это то, что делает IsPropertyMethod(). Если все на месте, вы выводите имя свойства в окно консоли. Метод Proceed() говорит Spring, что он должен продолжить с любым другим перехватчиком, который может захотеть что-то сделать с этим вызовом. Если этот перехватчик является последним в цепи, вызывается лежащая в основе реализация.

Вы можете проверить это с помощью простых определений класса:

public interface IClassWithData
{
	Guid GetData();
	Guid Data { get; }
}
public class ClassWithData
	: IClassWithData
{
	public ClassWithData()
		: base() { }
	public ClassWithData(Guid data)
		: base()
	{
		this.Data = data;
	}
	public Guid Data { get; private set; }
}

Следующий код использует перехватчик с классом ProxyFactory:

var factoryData = new ProxyFactory(new ClassWithData(Guid.NewGuid()));
factoryData.AddAdvice(new PropertyInterceptor());
var dataWithInterceptor = (IClassWithData)factoryData.GetProxy();
Console.Out.WriteLine(dataWithInterceptor.Data);

Класс ProxyFactory позволяет добавлять перехватчики в фабрику через AddAdvice(). Далее вы вызываете GetProxy(), и это возвращает динамический объект, реализующий интерфейс, к которому вы преобразуете возвращаемое значение. Как только используется свойство Data, вызывается перехватчик, и значение свойства выводится на консоль. Вы должны увидеть что-то похожее на рисунок 9-3.

Рисунок 9-3: Получение уведомления об использовании свойства. Когда свойство Data извлекается, вызывается метод перехватчика Invoke(), предоставляя вам возможность обеспечить дополнительную функциональность приложению (в данном случае, это вывод этого действия в окно консоли).

Хотя Spring.Aop обеспечивает достойный фреймворк для динамического кода, он несет в себе традиционные ограничения, как и большинство фреймворков: вы можете подключить только виртуальные члены или не-sealed классы. В следующем разделе вы будете использовать инструмент, который обеспечивает более высокий уровень гибкости в том, чтобы добавлять повторно используемые фрагменты кода.

Что такое PostSharp?

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

  • Добавление традиционного кода, такого как логгинга или трассировки для метода
  • Реализация методов и интерфейсов для класса

PostSharp предоставляет API, который упрощает применение кода в определенных точках приложения, без необходимости понимать низкоуровневые детали .NET. Он также интегрируется в Visual Studio, чтобы предоставить вам IDE помощников, так чтобы вы знали, когда на код влияет PostSharp. Основное отличие между PostSharp и Spring заключается в том, что PostSharp не ограничивается виртуальными элементами, потому что PostSharp может работать с виртуальными и не виртуальными методами. Далее в этой главе вы взломаете сборку, которая была модифицирована PostSharp, чтобы понять, что он делает в коде, а сейчас давайте напишем простой аспект, который сообщает вам, когда создается экземпляр класса.

Примечание

Если вы хотите поиграть с PostSharp, посетите www.sharpcrafters.com. Они предлагают платные и бесплатные версии своих продуктов, хотя вы быстро поймете, что бесплатная версия довольно ограничена. В этой книге вы увидите, что мы используем PostSharp, который требует платной версии, только в паре случаев. Мы считаем, стоит посмотреть на код, который использует функционал из платной версии, потому что тогда вы можете сделать некоторые интересные вещи. Мы ни в коем случае не настаиваем, чтобы вы купили PostSharp для запуска кода на основе платного функционала: мы не получаем никакого гонорара от создателей PostSharp.

Перехват создания объекта при помощи PostSharp

Первое, что вы сделаете с PostSharp, это напишете код, который скажет вам, когда вызывается конструктор, и укажет значения аргумента. Чтобы ничего не усложнять, вы выведете эту информацию в окно консоли. Рисунок 9-4 показывает, что делает этот аспект.

Рисунок 9-4: Добавление в код уведомлений о создании объекта. Всякий раз, когда вызывается конструктор, PostSharp внедряет код, так что информация о создании выводится в окне консоли.

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

Листинг 9-5: Работа с конструктором при помощи PostSharp
[Serializable]
public sealed class CreationAttribute
	: OnMethodBoundaryAspect
{
	public override void OnEntry(MethodExecutionArgs args)
	{
		if (args.Method.IsConstructor)
		{
			Console.Out.WriteLine(
				"Object {0} was instantiated with the following arguments:",
				args.Method.DeclaringType.Name);
			foreach (var argument in args.Arguments)
			{
				Console.Out.WriteLine("Type: {0} || Value: {1}",
				argument.GetType().Name, argument);
			}
		}
	}
}

Как говорилось в предыдущем разделе, PostSharp использует атрибуты, чтобы определить точки, где код как-либо будет изменен. В данном случае вы используете атрибут OnMethodBoundaryAspect. Обратите внимание, что PostSharp не следует соглашению по именованиям .NET для атрибутов. Вы, наверное, ожидали, что атрибут будет называться OnMethodBoundaryAspectAttribute, но это не так в случае с PostSharp.

PostSharp атрибуты имеют ряд переопределенных версий, которые можно использовать, чтобы узнать, когда происходит определенное событие в коде. Переопределив OnEntry(), вы получите уведомление, когда был вызван метод. А вам лишь интересен вызов конструктора, поэтому вы используете свойство IsConstructor для Method, что является ссылкой System.Reflection.MethodBase. Если это конструктор, вы отправляете имя метода на окно консоли вместе со всеми значениями аргумента. Обратите внимание, что свойство Arguments является коллекцией объектов; это не коллекция объектов наподобие чего-то вроде класса Argument, который предоставляет такую информацию, как имя и местоположение аргумента. Вы получаете значения аргументов.

Использование этого атрибута довольно простое:

[Creation]
public sealed class ClassWithCreation
{
	public ClassWithCreation()
		: base() { }
	public ClassWithCreation(Guid data)
		: base()
	{
		this.Data = data;
	}
	public Guid Data { get; private set; }
}

Теперь, все, что нужно, это написать подобный код, чтобы увидеть, как все работает:

var noData = new ClassWithCreation();
Console.Out.WriteLine(noData.Data);
var data = new ClassWithCreation(Guid.NewGuid());
Console.Out.WriteLine(data.Data);

На рисунке 9-5 показаны результаты запуска этого кода. В первом случае вызывается конструктор без аргументов, а во втором случае вы видите значения аргумента конструктора в окне консоли.

Рисунок 9-5: Использование PostSharp для перехвата создания объекта. PostSharp уведомит вас, когда будет создан объект указанного класса.

CreationAttribute является урезанной версией канонического примера, обычно наблюдаемого всякий раз, когда обсуждается АОП: трассировки. В данном случае вас интересует только вхождение конструктора, но OnMethodBoundaryAspect предоставляет OnExit, чтобы сообщить вам, когда метод завершит работу. Есть также переопределенная версия метода OnException, который можно использовать, если из метода выброшено исключение. Легко представить, как можно использовать эти методы, чтобы создать атрибут, который инкапсулирует аспект вхождения так, что вы можете быстро применить его к любому типу в вашем приложении.

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

[assembly: Creation]

Теперь у каждого класса в этом проекте будет уведомление об инициализации объекта! Причина связана с тем, как атрибуты работают в .NET. Вы можете указать, где будет использоваться атрибут, через AttributeUsageAttribute. Аспекты PostSharp не ограничивают вас определенным типом атрибутом, как OnMethodBoundaryAspect. Вы можете сказать PostSharp, чтобы он использовался в масштабе сборки, и вуаля: уведомления конструктора для каждого типа. Это довольно интересно.

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

Реализация Equals() и GetHashCode()

Вы видели, как вы можете перехватывать вызовы методов при помощи PostSharp. Не трудно представить себе, как можно использовать этот функционал, чтобы словить конкретные запросы метода для предоставления пользовательских реализаций методов во время выполнения. Допустим, вы хотите стандартным способом реализовать Equals() для всех классов путем сравнения значений всех открытых свойств. Вы можете создать пользовательский аспект, который следит за вызовом метода Equals(), вызывая код на основе рефлексии в этот конкретный момент. Но это не идеально, потому что вам придется перехватывать каждый вызов метода, чтобы найти нужный Equals(). Лучше сделать это в PostSharp через аспекты на уровне экземпляров и введение членов. Следующий листинг определяет EqualsAttribute, который обеспечивает пользовательскую реализацию Equals() для любого класса (наряду с GetHashCode(), потому что вы всегда должны переопределять оба при переопределении одного или другого).

Листинг 9-6: Реализация Equals() и GetHashCode()
[Serializable]
public sealed class EqualsAttribute
	: InstanceLevelAspect
{
	[IntroduceMember(IsVirtual = true,
		OverrideAction = MemberOverrideAction.OverrideOrIgnore,
		Visibility = Visibility.Public)]
	public override bool Equals(object obj)
	{
		var areEqual = false;
		if (obj != null && this.Instance.GetType()
			.IsAssignableFrom(obj.GetType()))
		{
			var result =
				(from prop in this.Instance.GetType().GetProperties(
				BindingFlags.Instance | BindingFlags.Public)
				where prop.CanRead
				select prop.GetValue(this.Instance, null)
				.Equals(prop.GetValue(obj, null)))
				.Distinct().ToList();
			areEqual = result.Count != 1 ? false : result[0];
		}
		return areEqual;
	}
	[IntroduceMember(IsVirtual = true,
		OverrideAction = MemberOverrideAction.OverrideOrIgnore,
		Visibility = Visibility.Public)]
	public override int GetHashCode()
	{
		return
			(from prop in this.Instance.GetType().GetProperties(
			BindingFlags.Instance | BindingFlags.Public)
			where prop.CanRead
			select prop.GetValue(this.Instance, null).GetHashCode())
			.Aggregate(0, (counter, item) => counter ^= item);
	}
}

Примечание

Для предыдущего примера нужна платная версия PostSharp.

Первым шагом является наследование пользовательского атрибута от InstanceLevelAspect. Это означает, что атрибут работает на уровне экземпляра, а не на уровне метода, как наш предыдущий CreationAttribute. На следующем этапе обеспечивается реализация Equals(). Для этого необходимо переопределить Equals() в самом атрибуте, а затем отметить этот метод IntroduceMemberAttribute. Это может показаться запутанным на первый взгляд, но то, что вы делаете, это буквально вводите маркированный член в экземпляр класса, для которого есть этот атрибут. Таким образом, любой класс с EqualsAttribute будет иметь любые вызовы Equals(), перенаправленные на этот метод Equal().

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

Это естественно – использовать this, но вы должны помнить, что вы пишете код, который нацелен на другой экземпляр объекта. Таким образом, вы используете свойство Instance, чтобы получить ссылку на объект в настоящее время в области видимости. Рисунок 9-6 показывает, как текущий объект раскрывается через свойство Instance.

Рисунок 9-6: Получение ссылки на "реальный" объект. В атрибуте вы используете свойство Instance, чтобы ссылаться на объект, для которого предназначен аспект. PostSharp обрабатывает для вас эту ссылку.

Метод GetHashCode() обрабатывается, как метод Equals() – он вводится в отмеченный объект. Все значения хэш кода "XORируются" при помощи функции Aggregate().

Теперь, когда у вас есть два переопределенных метода, их можно с легкостью добавить в класс:

[Equals]
public sealed class ClassWithEquals
{
	public int IntData { get; set; }
	public string StringData { get; set; }
}

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

var equals1 = new ClassWithEquals { IntData = 10, StringData = "10" };
var equals2 = new ClassWithEquals { IntData = 20, StringData = "20" };
var equals3 = new ClassWithEquals { IntData = 10, StringData = "10" };
Console.Out.WriteLine(equals1.Equals(equals2));
Console.Out.WriteLine(equals1.Equals(equals3));

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

Теперь вы знаете, что PostSharp может сделать метапрограммирование чистым и простым. Но вы можете быть удивлены тем, как PostSharp делает то, что он делает. В следующем разделе мы вкратце это рассмотрим.

Быстрое погружение в внутренний мир PostSharp

Как вы можете догадаться, нужно нечто большее, чем просто сослаться на PostSharp.dll, чтобы получить все эти модные «фишки», которые вы видели в двух последних разделах. PostSharp ввязывается в процесс компиляции, чтобы создать все необходимые трюки и пользовательскую реализацию, указанную вашим пользовательским атрибутом. Давайте посмотрим, что PostSharp делает с вашим кодом, рассмотрев класс ClassWithCreation, созданный в разделе 9.2.4. В следующем листинге показан код, существующий в конструкторе для ClassWithCreation, который принимает Guid, после того как PostSharp сделал свое дело.

Листинг 9-7: Код, измененный PostSharp
public ClassWithCreation(Guid data)
{
	this.<>z__InitializeAspects();
	MethodExecutionArgs methodExecutionArgs =
		new MethodExecutionArgs(null, new Arguments<Guid>
		{
			Arg0 = data
		});
	MethodExecutionArgs arg_2A_0 = methodExecutionArgs;
	MethodBase m = ClassWithCreation.<>z__Aspects.m7;
	arg_2A_0.Method = m;
	ClassWithCreation.<>z__Aspects.a4.OnEntry(methodExecutionArgs);
	this.Data = data;
}

Этот код был создан инструментом ILSpy (http://ilspy.net). PostSharp вставляет изрядное количество кода в метод для поддержки своих возможностей: вы ведь помните, что у оригинального метода была только одна строка кода: сеттер свойства. PostSharp также использует некоторые странные имена переменных, чтобы свести к минимуму вероятность того, что они будут конфликтовать с любыми именами, которые вы использовали. PostSharp сначала собирает значения аргументов при помощи ссылки MethodExecutionArgs. Затем он ловит ссылку MethodBase, которая относится к выполняемому в данный момент методу. Наконец, он передает их в метод OnEntry(), который вы переопределили в атрибуте CreationAttribute.

PostSharp имеет право менять то, как он выполняет свою модификацию кода, так что вы не обязательно должны строго придерживаться того, что вы видите здесь, ведь все меняется от версии к версии. Тем не менее, учитывая все, что вы знаете о метапрограммировании в .NET, вы, вероятно, не были удивлены, когда увидели, как PostSharp делает то, что он делает. Хотя PostSharp не дает вам возможность менять IL метода, для большинства АОП техник вам не нужен такой детальный контроль. Наличие API и набора инструментов, понятных для разработчиков, это все, что нужно, чтобы они начали выполнять всевозможные полезные модификации коде.