Главная страница   /   3.1. Дженерик-типы в качестве шаблонов (Метапрограммирование в .NET

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

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

Кевин Хазард

3.1. Дженерик-типы в качестве шаблонов

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

T max<T>(T left, T right)
{
	return left < right ? right : left;
}

Хотя это довольно симпатичная концепция того, как может быть написана функция max, она не будет компилироваться как C#, так как стандартный оператор C# «меньше-чем» (<) не может быть применен к дженерик аргументам. Можно переписать max как generic_max с помощью ограничения вроде этого:

T generic_max<T>(T left, T right)
	where T : IComparable<T>
{
	return (left.CompareTo(right) < 0) ? right : left;
}

Метод generic_max будет компилироваться как C# и работать для любого параметризованного типа, реализующего интерфейс IComparable<T> из-за ограничения, заданного where. Но как насчет тех типов, которые реализуют более старый интерфейс IComparable? Вы могли бы добавить еще одно ограничение для generic_max, чтобы решить проблему?

T generic_max<T>(T left, T right)
	where T : IComparable, IComparable<T>
{
	return (left.CompareTo(right) < 0) ? right : left;
}

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

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

let max left right = if left < right then right else left;;

max 1 2;; // max целых чисел возвращает 2
max 3.0 4.0;; // max вещественных чисел возвращает 4.0
max "hello" "world";; // max строк возвращает "world"

Эта функция max не имеет никаких маркеров дженерик синтаксиса в своем определении, но компилятор определенно будет считать ее дженерик. На самом деле, подпись для функции, возвращенная компилятором, читается так:

'a -> 'a -> 'a

Это F# довольно туманным способом говорит, что функция, заданная с двумя параметрами определенного типа, возвращает значение того же типа. Это звучит как дженерик, что на самом деле так и есть. Взглянув на определение функции, можно сказать, что нет никаких указаний на требуемый тип данных, выраженных или подразумеваемых в коде. F# компилятор сделал хорошую работу по поддержанию алгоритма нетронутыми без введения каких-либо видов ограничений для программиста. Магия этого, если таковая имеется, заключается в дженерик реализации стандартного оператора F# «меньше-чем» (<).

Если подумать, то становится очевидным, что дженерик функция F# max не является функцией в обычном смысле. Напротив, это своего рода модель или шаблон, выражающий намерения некоторых будущих функций, которые могут быть необходимы для работы с конкретными типами. Как доказало ограничение, добавленное к C# методу generic_max, шаблон зависит только от способности ранжировать два значения известного типа. В C# вы можете попробовать имитировать динамизм, который вы увидели в F# реализации, с помощью Reflection API. Таким образом, вы можете попробовать обойти проблему необходимости поддерживать интерфейс IComparable или интерфейс IComparable<T> как отдельные и в равной степени достаточные ограничения. Следующий листинг показывает попытку переписать функцию generic_max на dynamic_max, чтобы сделать это.

Листинг 3-1: dynamic_max как динамическая, дженерик (и неэффективная) функция max
public static T dynamic_max<T>(T left, T right)
{
	if (left is IComparable<T>)
		return ((left as IComparable<T>).CompareTo(right) < 0)
		? right : left;
	if (left is IComparable)
		return ((left as IComparable).CompareTo(right) < 0)
		? right : left;
	throw new ApplicationException(String.Format(
		"Type {0} must implement one of the IComparable or " +
		"IComparable<{0}> interfaces.", typeof(T).Name));
}

Дженерик функция dynamic_max начинается с тестирования параметризованного типа на реализацию IComparable<T>. Если операнд left реализует IComparable<T>, он приводится к этому типу, и вызывается член CompareTo, передавая операнд right в качестве аргумента. Это сравнение дает ранжирование, которое необходимо, чтобы определить, значение какого операнда больше. Если первый интерфейс не реализован, операнд left проверяется на реализацию не-дженерик интерфейса IComparable. Если реализован второй интерфейс, вызывается его функция CompareTo для ранжирования операндов. Если ни один из интерфейсов не поддерживается параметризованным типом, выбрасывается исключение.

Функция dynamic_max, показанная в листинге 3-1, по общему признанию неэффективна. Можете ли вы представить себе, что вам нужно делать все это с рефлексией каждый раз, когда нужно получить большее из двух значений? Возможно, вот такого плохого дизайна динамичного программного обеспечения вы и боялись, когда вы начинали ваше путешествие, чтобы стать метапрограммистом. Написания такого кода, как этот, следует избегать любой ценой.

Чтобы устранить проблему с производительностью dynamic_max, вы можете попробовать реализовать некий вид словаря, который кэширует стратегию сравнения для каждого встреченного типа во время выполнения. По крайней мере, дорогостоящая рефлексия будет выполняться только один раз для каждого параметризованного типа. Но это тоже слишком сложно. Из-за этого подхода напрашивается вопрос: "Нужно ли нам, чтобы функция max была полностью динамической во время выполнения"? В конце концов, если типы данных, с которыми вы работаете, хорошо известны во время компиляции, зачем вообще нужны все эти проблемы с созданием динамической функции?

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

Как оказалось, некоторые версии Microsoft Visual Studio 2008 и Microsoft Visual Studio 2010 и 2012 содержат такой инструмент, называемый Text Template Transformation Toolkit, или T4 для краткости. В этой главе мы покажем вам, как использовать T4 для генерации C#, XML, T-SQL, а также других видов кода в Visual Studio с использованием шаблонов, похожих на дженерики в C# или Visual Basic, с которыми вы, возможно, уже знакомы. Давайте начнем с того, что рассмотрим, как решить проблему создания набора вариантов функции max во время компиляции с использованием Т4.