Главная страница   /   3.2. Что такое T4 (Метапрограммирование в .NET

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

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

Кевин Хазард

3.2. Что такое T4

Вы будете вникать в тонкости T4 синтаксиса, по мере продвижения по разделу. Давайте сразу начнем с изучения листинга 3-2, в котором содержится шаблон T4, создающий класс greater. Цель класса заключается в обеспечении строго типизированных вариантов функции max, с которой мы поиграли в начале этой главы. Каждая из этих перегруженных функций в классе greater называется of, что кажется довольно странным на первый взгляд. Но именование является преднамеренным, так как, используя класс greater, вы сможете писать код, который читается гладко, как текст. Вот пример использования класса greater, чтобы получить большее из двух (greater of) целых чисел:

int x = 7, y = 11;
Console.WriteLine(
	"The larger of {0} and {1} is {2}.", x, y, greater.of(x, y));

Если вы добавите файл шаблона greater.tt в проект Visual Studio, она автоматически создаст файл greater.cs в качестве так называемого подчиненного файла, который будет компилироваться в рамках этого проекта. Использование T4 в Visual Studio является очень простым. Мы изучим некоторые детали реализации о способе, которым T4 интегрирован в Visual Studio, далее в этой главе.

Листинг 3-2: greater.tt в качестве шаблона T4 для генерации типизированных функций max
<#@ template language="C#" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#
	Type[] types_to_generate = new[]
	{
		typeof(object), typeof(bool), typeof(byte),
		typeof(char), typeof(decimal), typeof(double),
		typeof(float), typeof(int), typeof(long),
		typeof(sbyte), typeof(short), typeof(string),
		typeof(uint), typeof(ulong), typeof(ushort)
	};
#>
using System;
public static class greater
{
<#
	foreach (var type in types_to_generate)
	{
#>
	public static <#= type.Name #> of(<#= type.Name #> left, <#= type.Name #> right)
	{
<#
	Type icomparable =
		(from intf in type.GetInterfaces()
		where typeof(IComparable<>)
			.MakeGenericType(type)
			.IsAssignableFrom(intf)
			||
			typeof(IComparable).IsAssignableFrom(intf)
		select intf).FirstOrDefault();
	if (icomparable != null)
	{
#>
	return left.CompareTo(right) < 0 ? right : left;
<#
	}
	else
	{
#>
	throw new ApplicationException(
		"Type <#= type.Name #> must implement one of the " +
		"IComparable or IComparable<<#= type.Name #>> interfaces.");
<#
	}
#>
	}
<#
	}
#>
}

Именование файлов шаблонов T4

Примите к сведению тот факт, что шаблоны T4, как показано в листинге 3-2, обычно имеют расширение файла .tt (обозначая текстовый шаблон). Кто может сказать, почему не было выбрано расширение T4 для этих файлов? Возможно, .tt прижилось еще до того, как T4 стал обычным сокращением для данного инструмента.

Основы синтаксиса T4

Большинство шаблонных файлов T4 начинаются с одной или нескольких директив, заключенных в последовательность символов <#@ и #>. Если вы рассмастриваете Т4 как своего рода компилятор, то эти директивы действуют как параметры командной строки, которые можно использовать, чтобы управлять поведением компилятора и выходными данными. Вы узнаете о нескольких директивах в этой главе, но наиболее распространенными из них являются:

  • Template: используется для указания языка и опций компилятора
  • Output: используется для управления расширением выходного файла и кодировкой
  • Assembly: используется для ссылки на .NET сборки во время компиляции
  • Import: используется как директива import в VB и using в C#

После директив остальные строки в файле greater.tt являются частью так называемых управляющих блоков и текстовых блоков. Перед изучением того, как работают блоки управления и текстовые блоки, взгляните на листинг 3-3, который содержит часть выходных данных файла шаблона greater.tt. В качестве учебного упражнения попробуйте соотнести исходный код шаблона из листинга 3-2 со сгенерированным исходным кодом в листинге 3-3. Это упражнение даст вам представление о нюансах синтаксиса Т4. На вопросы, которые могут у вас появиться, когда вы будете делать это, мы дадим ответы в ближайшее время.

Листинг 3-3: greater.cs как сокращенный выход из шаблона greater.tt
using System;
public static class greater
{
	public static Object of(Object left, Object right)
	{
		throw new ApplicationException(
		"Type Object must implement one of the " +
		"IComparable or IComparable<Object> interfaces.");
	}
	public static Boolean of(Boolean left, Boolean right)
	{
		return left.CompareTo(right) < 0 ? right : left;
	}
	public static Byte of(Byte left, Byte right)
	{
		return left.CompareTo(right) < 0 ? right : left;
	}
	// Остальная часть сгенерированных функций "of"
	// была опущена для краткости. Каждая из них реализуется так же,
	// как версии для типов Boolean и Byte, показанных выше,
	// с простым вызовом CompareTo для ранжирования операндов.
}

Как вы можете видеть, класс greater содержит функцию of для каждого из типов, определенных в переменной types_to_generate, указанной в верхней части шаблона. Есть 15 таких функций в полном классе, потому что 15 типов включены в массив types_to_generate, который определен в первом управляющем блоке после директив.

Сравнение динамического кода со статическим

Сравните неэффективную функцию dynamic_max, показанную в листинге 3-1 с любой из строго типизированных функций of, выпущенных в класс greater. Подход с T4 создает намного больше кода, это очевидно. Сгенерированный код может быть громоздким, но (а) нет ничего динамического в коде, так что он будет более эффективным во время выполнения, и (б) в любом случае, вы не должны сами писать все эти функции. Мы использовали метаданные и T4, чтобы они были написаны для нас, что позволяет T4 отлично вписаться в наш «мешок» инструментов метапрограммирования для решения тех проблем, которые возникают при генерации кода.

Как вы можете видеть в листинге 3-3, первая функция в выходе, которое была выпущена для типа System.Object, генерирует исключение, потому что код управления шаблона определил, что тип System.Object не реализует ни один из необходимых интерфейсов IComparable<Object> или IComparable. Остальные 14 типов, включенных в массив types_to_generate, реализуют один или оба из необходимых интерфейсов сравнения, поэтому код выглядит следующим образом:

return left.CompareTo(right) < 0 ? right : left;

Этот код подходит для вызова дженерик реализации IComparable<T> для CompareTo или слабо типизированной реализации IComparable. Код LINQ to Objects в третьем стандартном блоке управления, показанном в шаблоне в листинге 3-2, делает это определение:

<#
	Type generic_icomparable =
		(from intf in type.GetInterfaces()
			let args = intf.GetGenericArguments()
			where intf.Name == "IComparable`1"
				&& args != null
				&& args[0].Equals(type)
			select intf).FirstOrDefault();
	if (generic_icomparable != null || type is IComparable)
	{
#>
	return left.CompareTo(right) < 0 ? right : left;

Если целевой класс реализует оба интерфейса, для нас не имеет значения, какой из них будет вызван, но правила C# компилятора выберут строго типизированную реализацию, потому что выпущенные параметры функции left и right будут строго типизированными. На данный момент все 14 методов of для совместимых типов имеют точно такую же реализацию. Далее в этой главе, когда вы настроите шаблон, чтобы добавить в него другие приемлемые ограничения, вы увидите некоторые различия, которые возникают в реализации функции of для 15 встроенных .NET типов.

Типы блоков T4

Теперь пришло время рассмотреть весь шаблон, чтобы понять, как работает движок T4. Давайте начнем с управляющего блока, который определяет переменную types_to_generate. Он следует сразу же за директивами в начале листинга 3-2 и выглядит следующим образом:

<#
Type[] types_to_generate = new[]
{
	typeof(object), typeof(bool), typeof(byte),
	typeof(char), typeof(decimal), typeof(double),
	typeof(float), typeof(int), typeof(long),
	typeof(sbyte), typeof(short), typeof(string),
	typeof(uint), typeof(ulong), typeof(ushort)
};
#>

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

Языки T4

На момент написания этой книги только C# и Visual Basic являлись поддерживаемыми языками управления, разрешенными в шаблонах T4 с использованием стандартного хоста. Выходной текст мог быть сгенерирован T4 для любого языка, например, T-SQL, F#, Java, XML и так далее. В конце концов, Т4 не является генератором кода. Это генератор текста, и пока целевой язык использует текст для своего исходного кода, T4 может создавать его.

Чтобы понять, как уникальная функция of была выпущена в сгенерированный класс для каждого из типов, определенных в массиве types_to_generate, найдите выражение foreach внутри второго блока управления в шаблоне в листинге 3-2. Оно выглядит вот так:

<#
	foreach (var type in types_to_generate)
	{
#>

Выражение foreach проходит циклом по типам, определенным ранее в массиве types_to_generate. Суть в том, что, хотя блоки управления T4 могут быть разделены текстовыми блоками и другими управляющими блоками, они все же являются частью одной и той же логикой управления шаблона. Процесс обработки идет сверху вниз, и на объекты, определенные в управляющем блоке T4, можно ссылаться в последующих блоках управления, подчиняясь правилам видимости выбранного языка управления.

Как T4 склеивает блоки шаблона

Возможно, лучшим способом понять, как T4 соединяет все блоки вместе в процессе трансформации, является наглядное представление первых двух блоков управления в верхней части листинга 3-2, показанных еще раз для удобства в листинге 3-4. Следующий листинг показывает все три блока вместе. Теперь сравните его с листингом 3-5, чтобы посмотреть, как T4 концептуально объединяет эти три блока в процессе трансформации.

Листинг 3-4: Два управляющих блока в T4, окружающих блок сырого текста
<#
	Type[] types_to_generate = new[]
	{
		typeof(object), typeof(bool), typeof(byte),
		typeof(char), typeof(decimal), typeof(double),
		typeof(float), typeof(int), typeof(long),
		typeof(sbyte), typeof(short), typeof(string),
		typeof(uint), typeof(ulong), typeof(ushort)
	};
#>
using System;
public static class greater
{
<#
	foreach (var type in types_to_generate)
	{
#>
Листинг 3-5: Как T4 концептуально объединяет блоки управления и текстовые блоки
Type[] types_to_generate = new[]
{
	typeof(object), typeof(bool), typeof(byte),
	typeof(char), typeof(decimal), typeof(double),
	typeof(float), typeof(int), typeof(long),
	typeof(sbyte), typeof(short), typeof(string),
	typeof(uint), typeof(ulong), typeof(ushort)
};
WriteLine("using System; ");
WriteLine("public static class greater");
WriteLine("{");
foreach (var type in types_to_generate)
{

Вы заметили, как строки текста между управляющими блоками были добавлены как выражения WriteLine в концептуальный, собранный код управления? Сравнение листингов 3-4 и 3-5 должно помочь вам понять внутреннюю работу хоста T4. T4 является своего рода текстовым компилятором, который чередует код в блоках управления с выражениями WriteLine для каждой строки в любом текстовом блоке, возникающем на этом пути. Результатом является единственный класс, который может быть скомпилирован и выполнен, чтобы превратить шаблон в выходной файл.

Понимание этого процесса сборки также поможет вам понять, почему вполне приемлема задняя открывающая фигурная скобка ({) в конце второго управляющего блока в листинге 3-2. Поскольку соответствующая закрывающая скобка (}) помещается в нужный блок управления, который появляются в шаблоне позже, результирующий класс, генерируемый T4, будет хорошо сформирован. На самом деле, для итерации foreach в шаблоне в листинге 3-2 соответствующая закрывающая скобка появляется как последний управляющий блок в файле, который выглядит следующим образом:

<#
	}
#>

Может быть, это странно, считать одну закрывающую фигурную скобку C# единственным содержанием в управляющем блоке T4, но это так. После того, как T4 собрал вместе все текстовые и управляющие блоки, определенные в шаблоне, эта закрывающая фигурная скобка для foreach не кажется уже такой одинокой и расположенной не к месту.

Управляющие блоки выражений в T4

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

public static <#= type.Name #> of(
	<#= type.Name #> left, <#= type.Name #> right)
{

При помощи синтаксиса управляющего блока выражения, свойство Name переменной type появляется три раза в сыром тексте. Эти управляющие блоки выражения являются удобным способом записать значения переменных или результатов вызовов функций в выходной файл без использования громоздких операторов Write внутри стандартных блоков управления. Результат выглядит намного чище, и он проще для чтения, что приводит к улучшению общего восприятия шаблона, когда программисту нужно понять, что делает шаблон.

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

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

Краткая история T4

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

Сейчас доступны несколько отличных сторонних инструментов для генерации кода на основе шаблонов. Некоторые из них предоставляют интеграцию с Microsoft Visual Studio. T4 имеет преимущество в том, что он работает с определенными версиями Microsoft Visual Studio 2008 и Microsoft Visual Studio 2010, поэтому большинство разработчиков программного обеспечения, которые создают коммерческие решения при помощи этих продуктов Microsoft, имеют к нему доступ без установки каких-либо дополнительных инструментов.

T4 стал весьма популярным в последние годы благодаря ряду факторов, и в основном из-за использования T4 различными командами по созданию инструментов отдела разработок компании Microsoft. Генераторы кода для ASP.NET MVC, ADO.NET Entity Framework, а также другие популярные фреймворки основаны на Т4, что делает его одним из наиболее широко используемых фреймворков для создания инструментов.

В том, что T4 считается фреймворком для создания инструментов, нет ничего удивительного, с тех пор как он впервые появился как часть Domain Specific Languages (DSL) Toolkit в 2005 году. Члены команды DSL постоянно экспериментирует с новыми языками программирования и связанными с ними концепциями. Для такой работы под рукой необходим гибкий, легко интегрируемый генератор кода. Примерно в то время, когда Гарет Джонс из DSL команды Microsoft осознал потребность в чем-то вроде T4 в 2004 году, члены DSL команды все еще делали большую часть своей работы по генерации кода, используя старомодные выражения printf в инструментах, написанных на языке С++. В C++ нет ничего плохого, но инструментам генерации кода, которые команда DSL использовала в то время, не хватало гибкости и простоты интеграции, которых требовало большинство их проектов.

Интервью с Гаретом Джонсом

Чтобы завершить эту главу, Гарет Джонс, создатель T4, представил интервью с авторами этой книги. История T4, изложенная здесь, показана без цитат. Но в блоге Гарета на http://blogs.msdn.com/b/garethj есть много отличных статей и ссылок на другие источники, которые подкрепят то, что вы узнаете от нас о T4. Гарет является одним из самых интересных людей, работающих на Microsoft, и мы надеемся, что вы будете следить за работой Гарета и узнаете больше о создании программного обеспечения на основе шаблонов.

Гарет посмотрел на технологии генерации кода, которые создали другие команды разработки программного обеспечения Microsoft, и обнаружил, что в движке обработки страниц ASP.NET было несколько ключевых моментов, которые искала его команда. С точки зрения синтаксиса, разделение разметки и кода управления было четким. Опыт Гарета помог ему понять, что наличие четкого синтаксического разделения сырого текста и кода может значительно улучшить понимание программистом инструмента генерации кода общего назначения. Что, возможно, еще более важно, архитектура движка ASP.NET, выполняя трансформацию страниц и создавая HTML путем сборки и выполнения, казалось, очень хорошо подходит для потребностей нескольких DSL проектов, в которые был вовлечен Гарет.

После вывода, что движок обработки страниц ASP.NET является хорошим кандидатом, чтобы стать универсальным инструментом создания кода, Гарет использовал движок ASP.NET в качестве основы для T4, совмещая его с другими компонентами, созданными командой DSL. Хотя T4 претерпел полное изменение с тех пор, как Гарет использовал движок ASP.NET в качестве основы для этого инструментария в 2004 году, сходство между традиционным синтаксисом страницы ASP.NET и синтаксисом T4 осталось. Любой разработчик, который работал с ASP или ASP.NET, распознает паттерны в Т4 и быстро их поймет. Если на то пошло, то любой разработчик, который работал с PHP, JSP и другими генераторами веб страниц также будет чувствовать себя, как дома, с Т4.

С самого начала T4 доступен как самостоятельный инструмент и как дополнение к Visual Studio. Как вы знаете, первая общедоступная версия T4 появилась в 2005 году как часть DSL Toolkit. Но поскольку загрузка DSL Toolkit не то, что делает обычный разработчик, относительно немногие разработчики знали об этой первой версии T4. Когда появилась Visual Studio 2008, T4 был сразу встроен в ее версии Professional и Ultimate. И в это время больше разработчиков начали использовать этот инструментарий, но его использование остается относительно низким по целому ряду причин.

С выпуском Visual Studio 2010 T4, наконец, вышел в центр внимания. Лучшей внедрение в интегрированную среду разработки (IDE), более тщательная документация по продуктам и появление некоторых корифеев в сообществе разработчиков помогли T4 заработать интерес тысяч программистов, которые никогда раньше не слышал об этот инструментарии. Самое главное, что в это же время несколько отделов команд разработчиков Microsoft начали использовать T4 для создания кода из метаданных для своих фреймворков.