Шаблоны
По мере усложнения проектов программисты все больше и больше нуждаются в
средствах повторного использования и переделки существующего базирующегося на
компонентах программного обеспечения. Чтобы достигнуть такого высокого уровня
повторного использования кода в других языках программирования, разработчики
обычно применяют возможность, называемую шаблонами. В C# будет включена
высокопроизводительная версия шаблонов с безопасными типами, мало отличающаяся в
синтаксисе и сильно отличающаяся в реализации от шаблонов, применяемых в C++, и
шаблонов, предлагаемых для языка программирования Java.
Построение шаблонных классов сегодня
Сегодня в C# программисты могут создавать ограниченные версии действительно
шаблонных типов, сохраняя данные в экземплярах типа шаблонного объекта.
Поскольку каждый объект в C# наследуется от типа шаблонного объекта и есть
возможность упаковки и распаковки общей системы типов .NET, программисты могут
сохранять и ссылки, и типы значений в переменной типа object.
Однако при преобразованиях между ссылочными типами, типами значений и типом
шаблонного объекта возникают потери производительности.
Проиллюстрируем это следующим примером кода, в котором создается простой тип
Stack с двумя операциями: "Push" и "Pop". Класс
Stack сохраняет свои данные в массиве типов
object, методы Push и Pop
используют базовый тип object, чтобы принимать и возвращать
данные, соответственно:
public class Stack
{
private object[] items = new object[100];
public void Push(object data)
{
...
}
public object Pop()
{
...
}
}
Затем произвольный тип — тип Customer, например, — может
быть добавлен в стек. Однако, если ваша программа должны извлекать данные,
необходимо явно привести результат метода Pop, типа базового
объекта, к типу Customer.
Stack s = new Stack();
s.Push(new Customer());
Customer c = (Customer) s.Pop();
Если в метод Push передается тип значения, например,
integer, среда выполнения автоматически преобразовывает его в ссылочный тип –
процесс, известный как упаковка – и затем сохраняет его во внутренней структуре
данных. Точно так же, если вы хотите, чтобы программа извлекала из стека тип
значений, такой как integer, ей необходимо явно преобразовывать тип объекта,
полученный из метода Pop, в тип значения, этот процесс известен
как распаковка:
Stack s = new Stack();
s.Push(3);
int i = (int) s.Pop();
Операции упаковки и распаковки между типами значений и ссылочными типами на
практике могут быть довольно тяжелы в плане производительности.
К тому же, в текущей реализации невозможно задать вид данных, помещаемых в
стек. Конечно стек может быть создан и затем тип Customer может
быть помещен в него. Позже этот же стек может использоваться и пытаться получить
данные и преобразовать их в другой тип, как в следующем примере:
Stack s = new Stack();
s.Push(new Customer());
Employee e = (Employee) s.Pop();
Однако, в то время как предыдущий код является примером неправильного
использования одного типа класса Stack, который нуждается в
дополнительной реализации и должна возникнуть ошибка, это действительно
допустимый код и компилятор не будет иметь проблем с ним. Во время выполнения
программа все-таки даст сбой из-за неправильной операции преобразования.
Создание и использование шаблонов
В C# шаблоны обеспечивают возможности создания высокопроизводительных
структур данных, которые специализируются компилятором на основе используемых
ими типах данных. Эти, так называемые, параметризованные типы создаются таким
образом, что их внутренние алгоритмы остаются теми же, но такими, что типы их
внутренних данных могут различаться в зависимости от предпочтений конечного
пользователя.
Чтобы минимизировать затраты на обучение для разработчиков, шаблоны в C#
объявляются в основном так же, как и в C++. Программисты могут создавать классы
и структуры точно так же, как они обычно это делали, а используя угловые скобки
(< and >) можно определить параметры типа. При использовании класса каждый
параметр должен быть замещен реальным типом, поставляемым пользователем
класса.
В следующем примере создайте класс Stack там, где определен
и объявлен в угловых скобках после объявления класса параметр типа
ItemType. Вместо того чтобы производить преобразования в и из
типа базового объекта, экземпляры базового класса Stack примут
тип, для которого они созданы. Параметр типа ItemType работает как
промежуточный элемент до тех пор, пока этот тип определен во время реализации и
используется как тип для внутреннего массива элементов – тип для параметра
метода Push и возвращаемый тип метода Pop:
public class Stack<ItemType>
{
private ItemType[] items;
public void Push(ItemType data)
{
...
}
public ItemType Pop()
{
...
}
}
Когда ваша программа использует класс Stack, как в следующем
примере, вы можете определить действительный тип, предназначенный для
использования шаблонным классом. В этом случае вы указываете классу
Stack использовать примитивный тип integer, определяя его как
параметр, используя угловую нотацию в выражении реализации:
Stack<int> stack = new Stack<int>();
stack.Push(3);
int x = stack.Pop();
Таким образом, ваша программа создает новый экземпляр класса
Stack, для которого каждый ItemType замещен
поставляемым integer параметром. Конечно, когда ваша программа создает новый
экземпляр класса Stack с integer параметром, внутренний массив
элементов в классе Stack теперь integer, а не
object. Кроме того, ваша программа устранила потери на
упаковку, связанные с помещением integer в стек. Более того, когда ваша
программа получает элемент из стека, вам больше не надо явно преобразовывать его
в соответствующий тип, потому что этот конкретный экземпляр класса
Stack использует integer в своей структуре
данных.
Если вы хотите, чтобы ваша программа сохраняла в классе
Stack элементы, отличные от integer, вы должны создать новый
экземпляр класса Stack, определяя новый тип как параметр.
Предположим, вы имеете простой тип Customer и хотите, чтобы
программа для его сохранения использовала объект Stack. Чтобы
сделать так, просто реализуйте классы Stack с объектом
Customer в качестве параметра типа и свободно повторно
используйте код вашей программы:
Stack<Customer> stack = new Stack<Customer>();
stack.Push(new Customer());
Customer c = stack.Pop();
Конечно, как только ваша программа создает класс Stack с
типом Customer в качестве параметра, она имеет возможность
сохранять только типы Customer в стеке. В C# шаблоны строго
типизированы, что означает, что вы не можете больше неправильно сохранять
integer в стеке, как это сделано в следующем примере:
Stack<Customer> stack = new Stack<Customer>();
stack.Push(new Customer());
stack.Push(3) // compile-time error
Customer c = stack.Pop(); // no cast required.
Преимущества шаблонов
Шаблоны дают возможность программистам создавать, тестировать и
распространять код однажды, а затем повторно его использовать для различных
типов данных. В то время как это истинно для первого примера
Stack, второй пример Stack позволяет вашей
программе повторно использовать код с незначительными потерями
производительности приложений. Для типов значений первый пример
Stack приводит к существенным проблемам с производительностью,
тогда как второй пример Stack полностью устраняет проблемы,
потому что вы ликвидировали упаковку и нисходящие приведение типов.
Кроме того, шаблоны проверяются во время компилирования. Когда ваша программа
создает экземпляр шаблонного класса с предоставленным параметром типа, он может
быть только того типа, который определила ваша программа при описании класса.
Например, после того как программа создала Stack объектов
Customer, она больше не могла помещать integer в стек.
Используя такое поведение, вы можете построить более надежный код.
Более того, реализация шаблонов в C# по сравнению с другими реализациями со
строгими типами сокращает разрастание кода. Создавая типизированные коллекции с
помощью шаблонов, вы можете избежать необходимости создания специальных версий
каждого класса, сохраняя при этом преимущества в производительности, возникающие
в случае создания таких версий. Например, ваша программа может создать один
параметризованный класс Stack и избежать необходимости создания
IntegerStack для сохранения integers,
StringStack для сохранения строк или
CustomerStack для сохранения типов
Customer.
Это, конечно, приводит к созданию более надежного кода. Создавая один класс
Stack, ваша программа может инкапсулировать все поведение,
связанное со стеком, в один удобный класс. Затем, когда вы создаете
Stack типов Customer, все еще очевидно, что
ваша программа использует стековую структуру данных, хотя и с сохраненными в ней
типами Customer.
Множество параметров типа
Шаблонные типы могут иметь любое количество типов параметров. В предыдущем
примере Stack использовался только один тип. Предположим, вы
создали простой класс Dictionary, который сохраняет значения
вместе с ключами. Ваша программа может определить шаблонную версию класса
Dictionary объявляя два параметра, разделенные запятыми в
угловых скобках описания класса.
public class Dictionary<KeyType, ValType>
{
public void Add(KeyType key, ValType val)
{
...
}
public ValType this[KeyType key]
{
...
}
}
При использовании класса Dictionary вы должны поставлять
множество параметров в угловых скобках выражения реализации, вновь разделенные
запятыми, и обеспечивать правильные типы параметрам функции Add
и индексатору:
Dictionary<int, Customer> dict = new Dictionary<int, Customer>();
dict.Add(3, new Customer());
Customer c = dict.Get[3];
Ограничения
Как правило, ваша программа будет делать больше, чем просто сохранять данные
на основе данного параметра типа. Зачастую вам захочется, чтобы ваша программа
использовала члены параметра типа для того, чтобы выполнять операторы в
шаблонном типе вашей программы.
Почему нужны ограничения
Предположим, что в методе Add класса
Dictionary вы хотите сравнивать элементы, используя метод
CompareTo предоставленного ключа, например:
public class Dictionary<KeyType, ValType>
{
public void Add(KeyType key, ValType val)
{
...
switch(key.CompareTo(x))
{
}
...
}
}
К сожалению, во время компиляции параметр KeyType, как ожидалось, не
определен. Написанный таким образом код, компилятор полагает, что ключевому
экземпляру параметра типа KeyType доступны только операции, доступные
типу шаблонного объекта, такие как ToString. В результате
компилятор выводит ошибку, потому что метод CompareTo не
определен. Тем не менее ваша программа может преобразовать ключевую переменную в
объект, содержащий метод CompareTo, такой как интерфейс
IComparable. В следующем примере ваша программа явно
преобразовывает экземпляр типа параметра KeyType, называемого ключом, в
интерфейс IComparable, что позволяет откомпилировать
программу:
public class Dictionary<KeyType, ValType>
{
public void Add(KeyType key, ValType val)
{
...
switch(((IComparable) key).CompareTo(x))
{
}
...
}
}
Однако, если теперь вы обрабатываете класс Dictionary и
поставляете параметр типа, который не реализует интерфейс
IComparable, возникнет ошибка времени выполнения, а именно
InvalidCastException.
Объявление ограничений
В C# ваша программа может поставлять дополнительный список ограничений для
каждого параметра типа, объявленного в шаблонном классе. Ограничения отображают
требования, которым должен удовлетворить тип для того, чтобы создать шаблонный
тип. Ограничения объявляются с помощью ключевого слова where,
за которым следует пара "параметр-требование", в которой "параметром" должен
быть один из параметров, определенных в шаблонном типе, а "требование" должно
быть классом или интерфейсом.
Для того, чтобы удовлетворить необходимости использования метода
CompareTo в классе Dictionary, ваша программа
может накладывать ограничение на параметр типа KeyType, требуя, чтобы в
первый параметр класса Dictionary для реализации интерфейса
IComparable передавался любой тип, например:
public class Dictionary<KeyType, ValType> where KeyType : IComparable
{
public void Add(KeyType key, ValType val)
{
...
switch(key.CompareTo(x))
{
}
...
}
}
Теперь после компиляции ваш код будет проверен для подтверждения того, что
всякий раз при использовании класса Dictionary ваша программа
передает в качестве первого параметра тип, реализующий интерфейс
IComparable. Кроме того, вашей программе больше не надо явно
преобразовывать переменную в интерфейс IComparable перед
вызовом метода CompareTo.
Множественные ограничения
Для любого данного параметра типа ваша программа может определить в качестве
ограничений любое количество интерфейсов, но не более чем один класс. Каждое
новое ограничение объявляется как еще одна пара параметр-требование; каждое
ограничение для данного шаблонного типа отделяется запятыми. В следующем примере
класс Dictionary содержит два типа параметров: KeyType
и ValType. Параметр типа KeyType имеет два
интерфейса-ограничения, в то время как у параметра типа ValType один
класс-ограничение:
public class Dictionary<KeyType, ValType> where
KeyType : IComparable,
KeyType : IEnumerable,
ValType : Customer
{
public void Add(KeyType key, ValType val)
{
...
switch(key.CompareTo(x))
{
}
...
}
}
Шаблоны в среде выполнения
Когда шаблонный класс откомпилирован, между ним и обычным классом фактически
нет никакой разницы. На самом деле, результатом компиляции является не что иное
как метаданные и промежуточный язык (IL). IL, конечно же, параметризован, чтобы
принимать поставляемые пользователем типы где-то в коде. Отличие в использовании
IL для шаблонного типа основано на том, является ли поставляемый параметр типа
типом значения или ссылочным типом.
При первичном создании шаблонного типа с типом значения в качестве параметра,
среда выполнения создает специализированный шаблонный тип с предоставленным
параметром (или параметрами), подставленными в соответствующие места в IL.
Специализированные шаблонные типы создаются единожды для каждого уникального
типа значения, используемого в качестве параметра.
Например, предположим в коде вашей программы объявлен Stack,
построенный из integers:
На этом этапе среда выполнения генерирует специализированную версию класса
Stack с целыми, подставленными соответственно его параметру.
Теперь, когда бы код вашей программы ни использовал Stack
целых, среда выполнения вновь и вновь использует сгенерированный
специализированный класс Stack. В следующем примере созданы два
экземпляра Stack целых, оба используют уже сгенерированный
средой выполнения код для Stack целых:
Stack<int> stackOne = new Stack<int>();
Stack<int> stackTwo = new Stack<int>();
Однако, если в другой точке кода вашей программы создается другой класс
Stack, на этот раз с другим типом значения, таким как long или
определенной пользователем структурой в качестве параметра, среда выполнения
генерирует другую версию шаблонного типа, на этот раз вставляя long в
соответствующие места IL. Преимуществом создания специализированных классов для
шаблонных конструкций с типами значений является лучшая производительность.
Как-никак, больше не нужны преобразования, потому что каждый специализированный
шаблонный класс содержит тип значения.
Немного иначе работают шаблоны для ссылочных типов. Как только впервые
создается шаблонный тип с любым ссылочным типом, среда выполнения создает
специализированный шаблонный тип с объектными ссылками, подставленными для
параметров в IL. Затем каждый раз при создании экземпляра построенного типа с
ссылочным типом в качестве параметра в зависимости от того, какой это тип, среда
выполнения вновь и вновь использует ранее созданную специализированную версию
шаблонного типа.
Например, предположим у вас есть два ссылочных типа, классы
Customer и Order, и далее предположим, что вы
создали Stack типов Customer:
Stack<Customer> customers;
На этом этапе среда выполнения генерирует специализированную версию класса
Stack, которая вместо сохранения данных сохраняет объектные
ссылки, которые будут заполняться позже. Предположим, следующая строка кода
создает Stack другого ссылочного типа, названного
Order:
Stack<Order> orders = new Stack<Order>();
В отличие от типов значений, для типа Order не создается
другая специализированная версия класса Stack. Экземпляр
специализированной версии класса Stack создается и задаются
ссылки на нужные объекты. Для каждой объектной ссылки, которая была подставлена
на место параметра типа, выделяется область памяти размером с тип
Order и устанавливается указатель на эту ячейку памяти.
Предположим, что затем вы добавили строку кода, чтобы создать
Stack типа Customer:
customers = new Stack<Customer>();
Как и при предыдущем использовании класса Stack, созданного
с типом Order, создается другой экземпляр специализированного
класса Stack, и содержащиеся в нем указатели ссылаются на
область памяти размером с тип Customer. Т.к. от программы к
программе количество ссылочных типов может широко варьироваться, реализация
шаблонов в C# сильно уменьшает количество кода, сокращая до одного количество
специализированных классов, создаваемых компилятором для шаблонных классов
ссылочных типов.
Более того, когда создается экземпляр шаблонного класса C# с параметром типа,
являющимся типом значения или ссылочным типом, во время выполнения он может
запрашиваться, используя reflection и оба его действительных типа, так же как
может быть установлен его параметр типа.
Различия между C# шаблонами и другими реализациями
Шаблоны С++ существенно отличаются от C# шаблонов. Там где C# шаблоны
компилируются в IL, что приводит к тому, что для каждого типа значения
специализация разумно происходит во время выполнения и однажды только для
ссылочных типов, шаблоны С++ являются по существу макросом расширения кода,
которые генерируют специализированный тип для каждого параметра типа,
поставляемого в шаблон. Кроме того, когда компилятор С++ сталкивается с
шаблоном, таким как Stack целых, он расширяет код шаблона в
класс Stack, содержащий целые как собственный тип. В
зависимости от того, является ли параметр типа типом значения или ссылочным
типом, за исключением компоновщика, специально разработанного для уменьшения
количества кода, каждый раз компилятор С++ создает специализированный класс, в
результате чего возникает существенное увеличение размера кода в сравнении C#
шаблонами.
Более того, шаблоны С++ не могут определять ограничения. Они могут только
неявно задать ограничения, просто используя член, который должен или не должен
принадлежать параметру типа. Если в параметре типа, который в итоге передается в
шаблонный класс, этот член существует, программа будет работать правильно. Если
член не существует, программа даст сбой и, возможно, будет возвращено непонятное
сообщение об ошибке. Поскольку C# шаблоны могут объявлять ограничения и строго
типизированы, таких потенциальных ошибок нет.
Между тем, компания Sun Microsystems® предложила дополнение к шаблонам в
следующей версии языка программирования Java под кодовым названием "Tiger".
Компания Sun выбрала реализацию, которая не требует изменения Виртуальной машины
Java.
Предложенная Java реализация использует синтаксис, подобный шаблонам в С++ и
шаблонов в C#, включая параметры типов и ограничения. Однако из-за того, что
типы значений и ссылочные типы интерпретируются по-разному, немодифицированная
Виртуальная машина Java не сможет поддерживать шаблоны для типов значений. По
существу, применения шаблонов в Java не приведет к увеличению эффективности
выполнения. Действительно, компилятор Java будет вводить автоматические
приведение типов из указанного ограничения, если оно объявлено, или базового
типа Object, если ограничение не объявлено, всякий раз, когда
необходимо будет возвращать данные. Более того, компилятор Java будет
генерировать отдельный специализированный тип во время компиляции, который затем
будет использовать для обработки любого созданного типа. И в заключение, т.к.
Виртуальная машина Java изначально не поддерживает шаблонов, не будет способа
обнаружить параметр типа для экземпляра базового типа во время выполнения, и
другие применения reflection будут жестко ограничены.
Поддержка шаблонов в других языках
У компании Microsoft есть намерение поддержать использование и создание
шаблонных типов в Visual J#™, Visual C++ и Visual Basic. В то время как
некоторые языки программирования могут реализовать эту возможность раньше, чем
другие, все три другие языка программирования Microsoft будут включать поддержку
шаблонов. Тем временем команда C# закладывает фундамент для возможности
многоязыковой поддержки. Microsoft тесно сотрудничает с партнерами для
обеспечения создания и использования шаблонов в языках программирования
.NET.
Итераторы
Итератор является языковой конструкцией, основанной на подобных возможностях
в таких исследовательских языках, как CLU, Sather и Icon. Итераторы облегчают
типам задание оператора foreach.
Зачем нужны итераторы
Сегодня, если классы хотят поддерживать итерирование, используя конструкцию
цикла foreach, они должны реализовать "enumerator" шаблон.
Например, конструкция цикла foreach, приведенная слева,
расширена компилятором в конструкцию цикла while, приведенную
справа:
Конструкция цикла foreach |
Конструкция цикла while |
List list = ...;
foreach(object obj in list)
{
DoSomething(obj);
}
|
Enumerator e = list.GetEnumerator();
while(e.MoveNext())
{
object obj = e.Current;
DoSomething(obj);
}
|
Таблица 1.
Обратите внимание, что структура данных List – итерируемый
элемент – должна поддерживать функцию GetEnumerator , для того
чтобы работал цикл foreach. Теперь, когда создана структура
данных List, должна быть реализована функция
GetEnumerator, которая затем возвратит объект
ListEnumerator:
public class List
{
internal object[] elements;
internal int count;
public ListEnumerator GetEnumerator()
{
return new ListEnumerator(this);
}
}
Созданный объект ListEnumerator должен не только
реализовывать свойство Current и метод
MoveNext, но и также управлять своим внутренним состоянием
таким образом, чтобы программа в ходе цикла могла каждый раз перемещаться на
следующий элемент. Эта машина внутреннего состояния может быть простой для
структуры данных List, но для структур данных, которые требуют рекурсивного
прохождения, таких как бинарные деревья, машина состояния может быть достаточно
сложной.
Т.к. реализация этого «enumerator» шаблона может потребовать больших затрат и
написания большого количества кода со стороны разработчика, C# будет включать
новую конструкцию, которая облегчит классу реализацию логики цикла
foreach.
Определение итератора
Поскольку итератор – это логический аналог конструкции цикла
foreach, он задается подобно функции, с помощью ключевого слова
foreach, за которым следует пара круглых скобок. В следующем
примере ваша программа объявит итератор для типа List.
Возвращаемый итератором тип определяется пользователем, но поскольку класс
List сохраняет внутри объектный тип, следующего примера
итератора будет возвращать объект:
public class List
{
internal object[] elements;
internal int count;
public object foreach()
{
}
}
Обратите внимание, что как только реализован «enumerator» шаблон, вашей
программе надо поддерживать машину внутреннего состояния, для того чтобы
отслеживать местоположение программы в структуре данных. У итераторов есть
встроенные машины состояний. Используя новое ключевое слово
yield, ваша программа может возвращать значения в оператор
foreach, вызвавший итератор. В следующий раз, когда снова
вызывает итератор, он начинает выполнение с того места, на котором остановился
предыдущий оператор yield. В следующем примере ваша программа
итерирует по трем строковыым типам:
public class List
{
internal object[] elements;
internal int count;
public string foreach()
{
yield "microsoft";
yield "corporation";
yield "developer division";
}
}
В следующем примере цикл foreach, который вызывает этот
итератор, будет выполнен три раза, каждый раз получая строки в порядке,
определенном тремя предыдущими операторами yield:
List list = new List();
foreach(string s in list)
{
Console.WriteLine(s);
}
Если вы хотите, чтобы программа реализовывала итератор для прохода элементов
списка, вы должны модифицировать итератор так, чтобы он проходил через массив
элементов, используя цикл foreach, возвращая каждый элемент
массива в каждой итерации:
public class List
{
internal object[] elements;
internal int count;
public object foreach()
{
foreach(object o in elements)
{
yield o;
}
}
}
Как работают итераторы
Итераторы выполняют рутинную работу по реализации «enumerator» шаблона от
имени вашей программы. Вместо того, чтобы создавать классы и машину состояния,
компилятор C#, используя «enumerator» шаблон, преобразовывает написанный в вашем
итераторе код в соответствующие классы и код. Таким образом, итераторы
обеспечивают существенное увеличение продуктивности разработчика.