- •Тема 2.8. Виртуальные функции в c#
- •Что такое «Виртуальные функции»?
- •Как объявляются виртуальные функции?
- •Абстрактные функции и абстрактные классы
- •Модификатор sealed
- •Реализация виртуального механизма
- •Класс с универсальными методами
- •Тема 2_09. Универсальные (параметризованные) классы. Шаблоны
- •Наследование и универсальность
- •Синтаксис универсального класса
- •Два основных механизма объектной технологии
- •Ограниченная универсальность
- •Синтаксис ограничений
- •Framework .Net и универсальность
Ограниченная универсальность
В языке C# допускаются три вида ограничений, накладываемых на родовые параметры.
• Ограничение наследования. where T: BaseClass, I1, ...Ik. Это основный вид ограничений, указывающий, что тип T является наследником некоторого класса и ряда интерфейсов. Следовательно, над объектами типа T можно выполнять все операции, заданные базовым классом и интерфейсами. Эти операции статический контроль типов будет разрешать и обеспечивать для них интеллектуальную поддержку, показывая список разрешенных операций. Ограничение наследования позволяет выполнять над объектами больше операций, чем в случае неограниченной универсальности.
• Ограничение конструктора. where T: new(). Это ограничение указывает, что тип T имеет конструктор без аргументов и, следовательно, позволяет создавать объекты типа T.
• Ограничение value/reference. where T: struct Это ограничение указывает, к значимым или к ссылочным типам относится тип T. Для указания значимого типа задается слово struct, для ссылочных - class.
Синтаксис ограничений
Если задан универсальный класс с типовыми параметрами T1, ... Tn, то на каждый параметр могут быть наложены ограничения всех типов.
Ограничения задаются предложением where, начинающимся соответствующим ключевым словом, после которого следует имя параметра, а затем через двоеточие - ограничения первого, второго или третьего типа, разделенных запятыми. Порядок их важен: если присутствует ограничение третьего типа, то оно записывается первым.
Заметьте, предложения where для разных параметров отделяются лишь пробелами; как правило, они записываются на отдельных строчках. Предложения where записываются в конце заголовка класса после имени и списка его типовых параметров, после родительских классов и интерфейсов, если они заданы для универсального класса. Вот синтаксически корректные объявления классов с ограничением универсальности:
public class Father<T1, T2> {
}
|
public class Base { public void M1() { } public void M2() { } }
|
public class Child<T1, T2> : Father<T1, T2> where T1 : Base, IEnumerable<T1>, new() where T2 : struct, IComparable<T2> { }
|
|
Класс Child с ограниченной универсальностью к данным типа T1 имеет право применять методы M1 и M2 базового класса Base; так же, как и методы интерфейса IEnumerable<T1>, он может создавать объекты типа T1, используя конструктор по умолчанию. Фактический тип, подставляемый вместо формального типа T2, должен быть значимым, и объекты этого типа разрешается сравнивать между собой.
ПРИМЕР: Список с возможностью поиска элементов по ключу
Возьмем классическую задачу построения списка с курсором.
Как и всякий контейнер данных, список следует сделать универсальным, допускающим хранение данных разного типа. С другой стороны, мы не хотим, чтобы в одном списке происходило смешение типов, - уж если там хранятся персоны, то чисел int в нем не должно быть. По этим причинам класс должен быть универсальным, имея в качестве параметра тип T, задающий тип хранимых данных. Мы потребуем также, чтобы данные хранились с их ключами. И поскольку не хочется заранее накладывать ограничения на тип ключей - они могут быть строковыми или числовыми, - то тип хранимых ключей будет еще одним параметром нашего класса. Поскольку мы хотим определить над списком операцию поиска по ключу, то нам придется выполнять проверку ключей на равенство, поэтому универсальность типа ключей должна быть ограниченной. Проще всего сделать этот тип наследником стандартного интерфейса IComparable.
Чтобы не затемнять ситуацию сложностью списка, рассмотрим достаточно простой односвязный список с курсором. Элементы этого списка будут принадлежать классу Node, два поля которого будут хранить ключ и сам элемент, а третье поле будет задавать указатель на следующий элемент списка. Очевидно, что этот класс должен быть универсальным классом. Вот как выглядит текст этого класса:
class Node<K, T> where K : IComparable<K>
{
public Node()
{
next = null; key = default(K); item = default(T);
}
public K key;
public T item;
public Node<K, T> next;
}
Рассмотрим теперь организацию односвязного списка.
public class OneLinkList<K, T> where K : IComparable<K>
{
Node<K, T> first, cursor; //первый и текущий элементы списка
// ...
// Операция поиска элемента по ключу
public bool findstart(K key)
{
Node<K, T> temp = first;
while (temp != null)
{
if (temp.key.CompareTo(key) == 0)
{
cursor = temp;
return (true);
}
temp = temp.next;
}
return (false);
}
}
В процессе поиска для каждого очередного элемента списка вызывается допускаемый ограничением метод CompareTo интерфейса IComparable. При отсутствии ограничений универсальности вызов этого метода или операции эквивалентности приводил бы к ошибке, обнаруживаемой на этапе компиляции.
Родовое порождение класса
Можно ли породить класс из универсального класса путем подстановки фактических параметров, а потом спокойно использовать этот класс обычным образом?
Это можно сделать обычным путем, подставив фактический тип вместо параметра. Например:
class IntStack : OneLinkStack<int> { ... }
Универсальные структуры
Так же, как и обычный класс, структура может иметь родовые параметры. Синтаксис объявления, ограниченная универсальность, другие детали универсальности естественным образом распространяются на структуры.
public struct Point<T>
{
T x, y; //координаты точки, тип которых задан параметром
// . . . другие свойства и методы структуры
}
Универсальные интерфейсы
Интерфейсы чаще всего следует делать универсальными, предоставляя большую гибкость для позднейших этапов создания системы. Возможно, вы заметили применение в наших примерах универсальных интерфейсов библиотеки FCL - IComparable<T> и других.
Универсальные делегаты
Делегаты также могут иметь родовые параметры. Чаще встречается ситуация, когда делегат объявляется в универсальном классе и использует в своем объявлении параметры универсального класса.
Универсальные делегаты с успехом используются при определении событий. В частности, класс EventHandler, применяемый для всех событий, не имеющих собственных аргументов, теперь дополнен универсальным аналогом, определенным следующим образом:
public void delegate EventHandler<T> (object sender, T args)
where T:EventArgs
Этот делегат может применяться и для событий с собственными аргументами, поскольку вместо параметра T может быть подставлен конкретный тип - потомок класса EventArgs, дополненный нужными аргументами.
