Главная страница   /   3.3. Более полезные примеры T4 (Метапрограммирование в .NET

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

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

Кевин Хазард

3.3. Более полезные примеры T4

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

Листинг 3-6: Chapter3Intro.cs динамически генерирует класс данных
using System;
using System.Collections.Generic;

class Chapter3Intro
{
	public static void GenerateDataClass(string className,
		List<Tuple<Type, string, bool>> properties,
		bool generateCtor = true)
	{
		Console.WriteLine("public class {0}", className);
		Console.WriteLine("{");
		foreach (var property in properties)
		{
			Console.WriteLine(
				" public {0} {1} {{ get; {2}set; }}",
				property.Item1,
				property.Item2,
				property.Item3 ? "" : "private ");
		}
		if (generateCtor)
		{
			Console.Write(" public {0}(", className);
			for (int ndx = 0; ndx < properties.Count; ndx++)
				Console.Write("{0}{1} {2}",
					(ndx > 0) ? ", " : "",
					properties[ndx].Item1,
					properties[ndx].Item2,
					properties[ndx].Item3);
				Console.WriteLine(")");
				Console.WriteLine(" {");
				foreach (var property in properties)
				{
					Console.WriteLine(
						" this.{0} = {0};",
						property.Item2);
				}
				Console.WriteLine(" }");
		}
		Console.WriteLine("}");
	}
}

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

Чтобы использовать генератор кода на основе C#, приведенный в листинге 3-1, давайте предположим, что вам нужно создать класс с именем DynamicCar. Автомобиль должен обладать свойствами для различных типов, представляющих обычные характеристики, такие как Make, Model, Year и MPG. Давайте ради интереса сделаем одно свойство только для чтения. Наконец, DynamicCar нужен конструктор, так что он тоже должен быть сгенерирован. Следующий листинг показывает, как можно вызвать функцию GenerateDataClass, показанную ранее, чтобы создать класс DynamicCar.

Листинг 3-7: Chapter3IntroMain.cs динамически генерирует класс
using System;
using System.Collections.Generic;
class Demo
{
	static void Main()
	{
		string className = "DynamicCar";
		bool generateCtor = true;
		var properties = new List<Tuple<Type, string, bool>>()
		{
			Tuple.Create(typeof(string), "Make", true),
			Tuple.Create(typeof(string), "Model", true),
			Tuple.Create(typeof(int), "Year", true),
			Tuple.Create(typeof(int), "MPG", false)
		};
		Chapter3Intro.GenerateDataClass(className,
			properties, generateCtor);
		Console.ReadLine();
	}
}

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

Листинг 3-8: Динамически сгенерированный класс DynamicCar
public class DynamicCar
{
	public System.String Make { get; set; }
	public System.String Model { get; set; }
	public System.Int32 Year { get; set; }
	public System.Int32 MPG { get; private set; }
	public DynamicCar(System.String Make,
		System.String Model, System.Int32 Year,
		System.Int32 MPG)
	{
		this.Make = Make;
		this.Model = Model;
		this.Year = Year;
		this.MPG = MPG;
	}
}

Сравнивая метаданные класса, которые были созданы, кажется, что вроде бы все в порядке. Класс DynamicCar имеет правильное имя. Все свойства, которые были описаны, включены, и каждое из них имеет правильный тип. Также свойство MPG, которое было отмечено как «только для чтения», имеет правильный сеттер. Наконец, обратите внимание, что конструктор тоже реализован правильно, устанавливая значения для каждого свойства из параметров конструктора.

Хотя генератора кода на основе C# работает правильно, такой подход демонстрирует ту проблему, с которой сталкивалась DSL команда Microsoft в 2004 году. Во многих случаях они использовали функции, такие как WriteLine, (а не printf в C++) для генерации кода таким образом. Вы можете ясно видеть, насколько громоздкой является модель, даже когда вы абстрагируете генератор кода в функцию, как GenerateDataClass. Здесь нет четкого разделения между стандартным текстом, который должен быть выпущен, и управляющим кодом, который работает над ним и вокруг него. Это отсутствие разделения уменьшает понимания, тормозит разработку и ведет к ошибкам, которые в противном случае можно избежать. Шаблоны, которые являются немного более сложными, чем показанный в этом примере, трудно реализовать и поддерживать при помощи такого подхода генерации кода. Более того, теряются реальные преимущества метапрограммирования.

Теперь посмотрим, как может быть использован Т4 для решения этой же проблемы (мы очень скоро перейдем к тому, как сделать это в Visual Studio). Изучите шаблон, показанный в следующем листинге. Этот шаблон T4 создаст тот же класс DynamicCar, показанный в листинге 3-3.

Листинг 3-9: GenerateDataClass.tt: шаблон T4 для генерации классов данных
<#@ template language="C#" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>
<#
	string className = "DynamicCar";
	bool generateCtor = true;
	var properties = new List<Tuple<Type, string, bool>>()
	{
		Tuple.Create(typeof(string), "Make", true),
		Tuple.Create(typeof(string), "Model", true),
		Tuple.Create(typeof(int), "Year", true),
		Tuple.Create(typeof(int), "MPG", false)
	};
#>
public class <#= className #>
{
<#
	foreach (var property in properties)
	{
#>
	public <#= property.Item1 #> <#= property.Item2 #> 
	{ 
		get; 
		<#= property.Item3 ? "" : "private " #>set;
	}
<#
	}
	if (generateCtor)
	{
#>
	public <#= className #>(<#
		for (int ndx = 0; ndx < properties.Count; ndx++)
			Write("{0}{1} {2}",
				(ndx > 0) ? ", " : "",
				properties[ndx].Item1,
				properties[ndx].Item2,
				properties[ndx].Item3);
#>)
		{
<#
		foreach (var property in properties)
		{
#>
	this.<#= property.Item2 #> = <#= property.Item2 #>;
<#
		}
	}
#>
	}
}

Этому шаблону очень просто следовать, но если вы никогда не работали с T4, некоторые синтаксические понятия стоит объяснить, чтобы сделать их более понятными. Прежде всего, три линии в начале шаблона, разделенные символами <#@ и #>, называются директивами. Это что-то похожее на параметры командной строки для компилятора. В данном случае, три директивы говорят компилятору следующее:

  1. Это шаблон, а языком, который вы будете использовать для управляющего код, будет C#
  2. Вы хотите импортировать пространство имен (как C# директива using), что позволит вам использовать дженерик типы коллекции, как List<T>.
  3. Генератор кода должен создать выходной файл с расширением .cs

После этих директивах идет блок, содержащий параметры, определяющие метаданные класса, который должен быть сгенерирован динамически. Эти параметры в точности такие, как показанные в листинге 3-2, которые были использованы до вызова функции генерации кода на основе C#. В следующей порции шаблона вы видите, как T4 позволяет вкрапливать текст в код и код в текст:

public class <#= className #>
{

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

В крошечном фрагменте шаблона, показанном ранее, есть три отдельных блока Т4. Между двумя текстовыми блоками есть встроенное выражение, которое выпускает переменную с именем className. Управляющие блоки выражения используют слегка измененные разделители <#= и #>. Приведенное выражение может быть переменной, литералом или вызовом функции и должно быть оценено как строка. Выражения в T4 наиболее удобны для вставки вычисленных значений или параметров в сырой текст без прерывания потока текста, как показано.

До сих пор вы видели, что T4 позволяет выполнять чистый код управления, выпускать сырой текст и вставлять выражения в сырой текст. Теперь давайте посмотрим на некоторые более интересные концепции управления потоком.

T4 обрабатывает шаблон сверху вниз. С эти просмотром «сверху-вниз» примера шаблона все, что выдаст T4, это объявление класса и фигурную скобку, которая следует за ним. Автоматические свойства будут сгенерированы следующими. Если вы посмотрите на то, как была сделана генерация свойств в генераторе кода из листинга 3-1, вы увидите цикл foreach, который перебирает метаданные и выводит текст с помощью выражения Console.WriteLine. Вот как это было сделано в C#:

foreach (var property in properties)
{
	Console.WriteLine(
	" public {0} {1} {{ get; {2}set; }}",
	property.Item1,
	property.Item2,
	property.Item3 ? "" : "private ");
}

Вы можете использовать этот же самый код в T4, вставив его непосредственно в блок управления. Но для демонстрации альтернатив этому подходу, доступных в T4, мы будем использовать технику, которая смешивает управляющий код, стандартный текст и выражения. Это вовсе не обязательно лучший подход, но в нем подчеркиваются некоторые из наиболее интересных особенностей синтаксиса Т4. Сравните следующие строки шаблона T4 с чистым эквивалентом C#, показанным ранее:

<#
	foreach (var property in properties)
	{
#>
	public <#= property.Item1 #> <#= property.Item2 #> 
	{
		get; 
		<#= property.Item3 ? "" : "private " #>set; 
	}
<#
	}
#>

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

public System.String Make { get; set; }
public System.String Model { get; set; }
public System.Int32 Year { get; set; }
public System.Int32 MPG { get; private set; }

Вы заметили, как были использованы два отдельных блока управления T4, чтобы «обернуть» смешанный набор текстовых блоков и выражений и создать этот выход? На самом деле, закрывающая фигурная скобка цикла foreach появляется в отдельном управляющем блоке. Это может показаться странным, но в Т4 это вполне приемлемо. Первый текстовый блок между этими блоками управления выпускает ключевое слово C# public, затем используется выражение, которое ссылается на переменную диапазона, определенную в цикле foreach. Техника смешивания выражений и сырого текста завершает объявления для каждого свойства.

Когда вы начинаете работать в T4, такой вид конструкции может показаться довольно запутанным. Ваше нормальное представление об областях видимости в таких языках как C# или Visual Basic не совсем сюда подходит. Главное, что нужно помнить, что разделители <# и #>, используемые для управляющих блоков Т4, действительно создают области видимости для кода управления шаблона. Синтаксически, однако, эта область видимости не ограничена одной парой разделителей. Вы можете свободно разделить область видимости, как вам нравится, смешивая текст, несколько блоков управления и выражения, как вы считаете нужным.

В последнем разделе шаблона, приведенном в листинге 3-9, создается конструктор класса. Технические приемы, которые вы видели до сих пор, используются снова для перебора списка свойств еще два раза. Первая итерация создает список параметров в объявлении конструктора, при второй итерации эти параметры присваиваются связанным с ними свойствам в теле конструктора. Первый из этих циклов вводит вспомогательную функцию T4 Write, которая во многом похожа на Console.WriteLine, что используется в генераторе кода на основе C#. Но функция Т4 Write вставляет текст в выходной файл, вместо того чтобы отправить его на консоль.

Если добавить файл шаблона из листинга 3-9 в проект Visual Studio, вы увидите, что при каждом его изменении и сохранении, генерируется файл с исходным кодом, содержащий класс DynamicCar. Выходной файл будет иметь такое же имя, как и шаблон, а расширение будет находиться в директиве <#@output#>.

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

Шаблоны должны быть красивыми

Концепция смешивания разметки и кода вообще не нова. В мире веб, маленькая идея, задуманная в 1994 году и первоначально известная как инструменты Personal Home Page (PHP) превратилась в технологического «монстра», который и привел такие имена, как Facebook, Joomla, Drupal и Digg к славе и богатству. Active Server Pages (ASP), который Microsoft выпустила в 1998 году, использовал ту же технику смешивания разметки и кода, чтобы обрабатывать свои веб страницы. JavaServer Pages (JSP), выпущенный в 1999 году, и ASP.NET в 2002 году следуют тому же базовому паттерну. Это не удивительно, что любой, кто кодировал на PHP, ASP, JSP или ASP.NET будет чувствовать себя, как дома, с Т4.

В последние годы многие разработчики Microsoft стали критичными по отношению к синтаксису ASP.NET, от которого был унаследован Т4. Некоторые жалуются, что он слишком «неуклюжий» или слишком декоративный для эффективного макета веб страницы, в частности, при создании крупных, сложных страниц. Разделение разметки и управляющего кода слишком тяжело для вкусов многих современных веб разработчиков. Такие генераторы разметки, как HTML Abstraction Markup Language (HAML), и движки представления MVC Razor и Spark появились в последние годы, чтобы удовлетворять тому требованию, что разметка должна быть как функциональной, так и красивой.

Какие последствия это имеет для T4, который использует старый, возможно, тяжелый синтаксис для разделения кода управления и стандартного текста? Является ли T4 как функциональным, так и красивым? Должен ли T4 отказаться от ASP синтаксиса и принять что-то более обтекаемое, как Razor? Ответ от Microsoft на этот последний вопрос – нет. Что кажется желательным для веб разработчиков не всегда идеально в других случаях. Когда вам нужно смешать шаблонный кода на C# или Visual Basic с кодом управления или кодом выражений, написанными на тех же языках, наличие некой декоративности в синтаксисе шаблона может быть хорошей вещью. Если бы у Т4 был Razor-подобный синтаксис, разделение текста и кода управления в шаблоне было бы почти неразличимым. И это бы значительно снизило понимание. Даже с хорошим синтаксическим разделением, есть места в листинге 3-9, например, где трудно сказать, которая из фигурных скобок относится к текстовому блоку или блоку управления. Если попробовать это с Razor синтаксисом, вы бы чесали затылок, пытаясь понять это.

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

Движок Т4, с другой стороны, не знает или ему все равно, какой выход он генерирует. Он совершенно не в курсе о типе выходного файла или любом синтаксическом соглашении того, что вы пытаетесь создать. За исключением горстки вспомогательных функций для обработки отступов и «выпуска» текста, синтаксис T4 не получает слишком много поддержки от движка, при которой можно что-то оптимизировать. Означает ли это, что другой парсер синтаксиса не может быть адаптирована к T4? Нет, но скорее всего T4, будет в обозримом будущем использовать свой «неуклюжий», декоративный синтаксис. Это субъективная оценка, но как текстовый генератор общего назначения Т4 может удерживать хороший баланс между функцией и формой.