Главная страница   /   7.1. Случай для внедрения кода (Метапрограммирование в .NET

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

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

Кевин Хазард

7.1. Случай для внедрения кода

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

Повторяющиеся реализации паттернов кодирования

В разработке программного обеспечения принято наталкиваться на реализации в коде, которые повторяются в приложении, но не следуют идиомам поддержки этого дублирования. Пример ToString(), используемый в различных местах этой книги, чтобы продемонстрировать идеи метапрограммирования, является простым примером. Вы можете скопировать и вставить жестко закодированный пример в каждый класс, где вы хотите иметь те же результаты паттерна ToString(). Вы также можете использовать рефлексию, Reflection.Emit или выражения для создания этой реализации во время выполнения. Но лучше всего получить лучшее из обоих миров. В следующем фрагменте кода содержится намек на то, что будет в разделе 7.2. Вы добавляете атрибут в код, и реализация внедрена в сборку:

[ToString]
public sealed class AttributedCustomer : Customer
{
	// ...
}

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

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

Листинг 7-1: Трассировка выполнения метода
public static int Divide(int x, int y)
{
	Console.Out.WriteLine("Divide started");
	Console.Out.WriteLine("x = " + x);
	Console.Out.WriteLine("y = " + y);
	if(y == 0)
	{
		Console.Out.WriteLine("Divide threw an ArgumentException");
		throw new ArgumentException();
	}
	var result = x / y;
	Console.Out.WriteLine("Divide finished - return = " + result);
	return result;
}

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

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

Разве не лучше иметь подобный код?

[Trace]
public static int Divide(int x, int y)
{
	if(y == 0)
	{
		throw new ArgumentException();
	}
	return x / y;
}

Он гораздо чище. Вы позволяете процессу динамического внедрения кода выяснить, что добавить в метод, чтобы провести его трассировку, основываясь на существовании атрибута (или другом подходе, основанном на конфигурации), а вы можете сосредоточиться на реализации.

Реструктуризация кода

Еще одна уловка, которую вы можете сделать с переписыванием, – это передвижение кода для поддержки конкретных подходов кодирования. Это иллюстрируется с введением идеи программирования по контракту в .NET Framework через контракты для кода (Code Contracts). Программирование по контракту является способом, которым разработчики указывают пред- и постусловия в методах наряду с инвариантами в объектах. Следующий фрагмент кода показывает метод, который требует, чтобы данный id не был нулевым и чтобы возвращаемое значение являлось не-null:

public static Customer Create(uint id)
{
	Contract.Requires(
		id > 0, "The ID must be greater than zero.");
	Contract.Ensures(Contract.Result<Customer>() != null);
	return new Customer(id);
}

Вызов Contract.Requires() обрабатывает предусловие (параметр id должен быть ненулевым), а Contract.Ensures() обеспечивает постусловие, что возвращаемое значение будет не-null. Что может показаться немного странным, так то, что постусловие предшествует вызову return. Как данный код собирается дать эту гарантию?

Это делается при помощи магии переписывания кода. Поскольку Code Contracts — это только API и не поддерживается такими языками как C# или VB с помощью ключевых слов, необходимо установить инструмент в Visual Studio, чтобы переписать IL поток в метод, после того как компилятор сделал свое дело. Как только это сделано, поток кода меняется (если проследить это в отладчике) из

  • Открытие фигурных скобок
  • Возврат
  • Закрытие фигурных скобок

на

  • Contract.Requires
  • Открытие фигурных скобок
  • Возврат
  • Закрытие фигурных скобок
  • Contract.Ensures

Даже если все еще кажется, что метод "возвращает" значение перед тем, как оценивается постусловие, это не так. Вы должны убедиться, что постусловие правильное, прежде чем метод вернет значение. Но вы не можете поместить любой код после оператора return – C# компилятор не позволит. Таким образом, разработчики Code Contract написали свой инструмент, чтобы переписать выполнение кода таким образом, чтобы код выполнялся правильно; но выглядит так, что сначала "оценивается" оператор return, а затем выполняется постусловие. Как вы можете догадаться, переписать поток кода без нарушения первоначальных намерений разработчика – не тривиальное дело.

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

Совет

Если вы никогда не сталкивались с концепцией программирования по контракту, и вы хотели бы узнать больше, вам стоит посетить два места. Одним из них является официальный сайт от Microsoft: http://research.microsoft.com/en-us/projects/contracts/. Code Contracts встроены в версию 4.0 .NET Framework (под System.Diagnostics.Contracts), но вам нужны инструменты с этого сайта, чтобы получить полный эффект. Другой ресурс представляет собой серию статей, написанных одним из нас (Кевином). Вы можете найти их на http://mng.bz/04zB.

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