Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
C# - лекции IntUit (Биллиг В.А.).pdf
Скачиваний:
140
Добавлен:
13.02.2015
Размер:
4.13 Mб
Скачать

Рис. 22.3. Три разных стека, порожденных абстрактным универсальным классом

Дополним наше рассмотрение еще одним примером работы с вариацией стеков, в том числе хранящим объекты класса Person:

public void TestPerson()

{

OneLinkStack<int> stack1 = new OneLinkStack<int>(); OneLinkStack<string> stack2 = new OneLinkStack<string>(); ArrayUpStack<double> stack3 = new ArrayUpStack

<double>(10);

ArrayUpStack<Person> stack4 = new ArrayUpStack<Person>(7); stack2.put("Петров"); stack2.put("Васильев");

stack2.put("Шустов");

stack1.put(27); stack1.put(45); stack1.put(53); stack3.put(21550.5); stack3.put(12345.7);

stack3.put(32458.8);

stack4.put(new Person(stack2.item(), stack1.item(), stack3.item()));

stack1.remove(); stack2.remove(); stack3.remove(); stack4.put(new Person(stack2.item(), stack1.item(),

stack3.item()));

stack1.remove(); stack2.remove(); stack3.remove(); stack4.put(new Person(stack2.item(), stack1.item(),

stack3.item()));

Person pers = stack4.item(); pers.PrintPerson(); stack4.remove(); pers = stack4.item(); pers.PrintPerson(); stack4.remove(); pers = stack4.item(); pers.PrintPerson(); stack4.remove(); if (stack4.empty()) Console.WriteLine("OK!");

}

Результаты работы этой процедуры приведены на рис. 22.4.

Рис. 22.4. Работа со стеками

Ограниченная универсальность

Хорошо, когда есть свобода. Еще лучше, когда свобода ограничена. Аналогичная ситуация имеет место и с универсальностью. Универсальность следует ограничивать. На типы универсального класса, являющиеся его параметрами, следует накладывать ограничения. Звучит парадоксально, но, наложив ограничения на типы, программист получает гораздо большую свободу в работе с объектами этих типов.

Если немного подумать, то это совершенно естественная ситуация. Когда имеет место неограниченная универсальность, над объектами типов можно выполнять только те операции, которые допускают все типы, - в C# это эквивалентно операциям, разрешенным над объектами типа object, прародителя всех типов. В нашем предыдущем примере, где речь шла о свопинге, над объектами выполнялась

единственная операция присваивания. Поскольку присваивание внутри одного типа разрешено для всех типов, то неограниченная универсальность приемлема в такой ситуации. Но что произойдет, если попытаться выполнить сложение элементов, сравнение их или даже простую проверку элементов на равенство? Немедленно возникнет ошибка еще на этапе компиляции. Эти операции не разрешены для всех типов, поэтому в случае компиляции такого проекта ошибка могла бы возникнуть на этапе выполнения, когда вместо формального типа появился бы тип конкретный, не допускающий подобную операцию. Нельзя ради универсальности пожертвовать одним из важнейших механизмов C# и Framework .Net - безопасностью типов, поддерживаемой статическим контролем типов. Именно поэтому неограниченная универсальность существенно ограничена. Ее ограничивает статический контроль типов. Бывают, разумеется, ситуации, когда необходимо на типы наложить ограничения, позволяющие ослабить границы статического контроля. На практике универсальность почти всегда ограничивается, что, повторяю, дает большую свободу программисту.

В языке C# допускаются три вида ограничений, накладываемых на родовые параметры.

Ограничение наследования. Это основный вид ограничений, указывающий, что тип TT является наследником некоторого класса и ряда интерфейсов. Следовательно, над объектами типа TT можно выполнять все операции, заданные базовым классом и интерфейсами. Эти операции статический контроль типов будет разрешать и обеспечивать для них интеллектуальную поддержку, показывая список разрешенных операций. Ограничение наследования позволяет выполнять над объектами больше операций, чем в случае неограниченной универсальности. Синтаксически ограничение выглядит так: where T: BaseClass, I1, ...Ik.

Ограничение конструктора. Это ограничение указывает, что тип TT имеет конструктор без аргументов и, следовательно, позволяет создавать объекты типа T.T Синтаксически ограничение выглядит так: where T: new().

Ограничение value/reference. Это ограничение указывает, к значимым или к ссылочным типам относится тип T.T Для указания значимого типа задается слово struct, для ссылочных - class. Так что синтаксически этот тип ограничений выглядит так: where T: struct.

Возникает законный вопрос: насколько полна предлагаемая система ограничений? Конечно, речь идет о практической полноте, а не о математически строгих определениях. С позиций практики систему хотелось бы дополнить, в первую очередь, введением ограничений операций, указывающим допустимые знаки операций в выражениях над объектами соответствующего типа. Хотелось бы, например, указать, что к объектам типа TT применима операция сложения + или операция сравнения <. Позже я покажу, как можно справиться с этой проблемой, но предлагаемое решение довольно сложно. Наличие ограничения операций намного элегантнее решало бы эту проблему.

Синтаксис ограничений

Уточним некоторые синтаксические правила записи ограничений. Если задан

универсальный класс с типовыми параметрами 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,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;

}

Класс Node имеет два родовых параметра, задающих тип ключей и тип элементов. Ограничение на тип ключей позволяет выполнять их сравнение. В конструкторе класса поля инициализируются значениями по умолчанию соответствующего типа.

Рассмотрим теперь организацию односвязного списка. Начнем с того, как устроены его данные:

public class OneLinkList<K, T> where K : IComparable<K>

{

Node<K, T> first, cursor;

}

Являясь клиентом универсального класса Node, наш класс сохраняет родовые параметры клиента и ограничения, накладываемые на них. Два поля класса - first и cursor - задают указатели на первый и текущий элементы списка. Операции над списком связываются с курсором, позволяя перемещать курсор по списку. Рассмотрим вначале набор операций, перемещающих курсор:

public void start()

{cursor = first; } public void finish()

while (cursor.next != null) cursor = cursor.next;

}

public void forth()

{if (cursor.next != null) cursor = cursor.next; }

Операция start передвигает курсор к началу списка, finish - к концу, а forth - к следующему элементу справа от курсора. Операции finish и forth определены только для непустых списков. Конец списка является барьером, и курсор не переходит через барьер. Нарушая принципы ради краткости текста, я не привожу формальных спецификаций методов, записанных в тегах <summary>.

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

public void add(K key, T item)

{

Node<K, T> newnode = new Node<K, T>(); if (first == null)

{

first = newnode; cursor = newnode; newnode.key = key; newnode.item = item;

}

else

{

newnode.next = cursor.next; cursor.next = newnode; newnode.key = key; newnode.item = item;

}

}

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

Рассмотрим теперь операцию поиска элемента по ключу, реализация которой потребовала ограничения универсальности типа ключа K:

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);

}

Искомые элементы разыскиваются во всем списке. Если элемент найден, то курсор устанавливается на найденном элементе и метод возвращает значение true. Если элемента с заданным ключом нет в списке, то позиция курсора не меняется, а метод возвращает значение false. В процессе поиска для каждого очередного элемента списка вызывается допускаемый ограничением метод CompareTo интерфейса IComparable. При отсутствии ограничений универсальности вызов этого метода или операции эквивалентности приводил бы к ошибке, обнаруживаемой на этапе компиляции.

Два метода класса являются запросами, позволяющими извлечь ключ и элемент списка, который отмечен курсором:

public K Key()

{

return (cursor.key);

}

public T Item()

{

return(cursor.item);

}

Давайте рассмотрим теперь тестирующую процедуру - клиента нашего списка, демонстрирующую работу со списками, в которых элементы и ключи имеют разные типы:

public void TestConstraint()

{

OneLinkList<int, string> list1 = new OneLinkList <int, string>();

list1.add(33, "thirty three"); list1.add(22, "twenty two"); if(list1.findstart(33)) Console.WriteLine

("33 - найдено!");

else Console.WriteLine("33 - не найдено!");

if (list1.findstart(22)) Console.WriteLine ("22 - найдено!"); else Console.WriteLine("22 - не найдено!");

if (list1.findstart(44)) Console.WriteLine ("44 - найдено!"); else Console.WriteLine("44 - не найдено!");

Person pers1 = new Person("Савлов", 25, 1500); Person pers2 = new Person("Павлов", 35, 2100); OneLinkList<string, Person> list2 = new OneLinkList

< string, Person>();

list2.add("Савл", pers1); list2.add( "Павел", pers2); if (list2.findstart("Павел")) Console.WriteLine

("Павел - найдено!");

else Console.WriteLine("Павел - не найдено!"); if (list2.findstart("Савл")) Console.WriteLine

("Савл - найдено!");

else Console.WriteLine("Савл - не найдено!"); if (list2.findstart("Иоанн")) Console.WriteLine

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]