Проект к данной лекции Вы можете скачать здесь.
Необходимость в универсализации возникает с первых шагов программирования. Одна из первых процедур, появляющихся при обучении программированию, - это процедура свопинга:обмен значениями двух переменных одного типа. Выглядит она примерно так:
public void Swap(ref T x1, ref T x2)
{
T temp;
temp = x1; x1 = x2; x2 = temp;
}
Если тип T - это вполне определенный тип, например, int, string или Person, то никаких проблем не существует, все совершенно прозрачно. Но как быть, если возникает необходимость обмена данными и типа int, и типа string, и типа Person? Неужели нужно писать копии этой процедуры для каждого типа? Проблема легко решается в языках, где нет
В Swap. В строго типизированном языке C#, скажете Вы, есть универсальный тип object, переменным которого можно присваивать значения любого типа, что позволяет написать следующую процедуру обмена значениями:
public void SwapObject( ref object item1, ref object item2)
{
object temp = item1;
item1 = item2; item2 = temp;
}
Эта процедура нормально компилируется, но, заметьте, ей нельзя передать аргументы, тип которых отличается от object. Дело в том, что ref object соответствует только такой же тип object в конкретный тип T должно быть явным. Так что использовать этот метод клиент может, если только сам будет выполнять приведение типа.
В примерах, приводимых ниже, как обычно, построено решение с именем Ch8. В это решение вложен консольный проект ConsoleGeneric. В проект добавлен класс Generic, содержащий методы, подлежащие исследованию, в частности метод SwapObject. В проект также добавлен класс Testing, являющийся клиентом класса Generic:
class Testing
{
Generic gen = new Generic();
//тестирующие методы
}
Рассмотрим пример тестирования работы метода SwapObject:
public void TestSwapObject()
{
int n1 = 7, n2 = 11;
double x1 = 7.7, x2 = 11.1;
Console.WriteLine("n1 = {0}, n2 = {1}", n1, n2);
Console.WriteLine("x1 = {0}, x2 = {1}", x1, x2);
Console.WriteLine("Попытка прямого обмена данными одного типа");
// gen.SwapObject(ref n1, ref n2);
// gen.SwapObject(ref x1, ref x2);
Console.WriteLine("заканчивается ошибкой еще на этапе компиляции!");
Console.WriteLine("Необходимо явное приведение к типу! ");
object obj1, obj2;
obj1 = n1; obj2 = n2;
gen.SwapObject(ref obj1, ref obj2);
n1 = (int)obj1; n2 = (int)obj2;
Console.WriteLine("n1 = {0}, n2 = {1}", n1, n2);
obj1 = x1; obj2 = x2;
gen.SwapObject(ref obj1, ref obj2);
x1 = (double)obj1; x2 = (double)obj2;
Console.WriteLine("x1 = {0}, x2 = {1}", x1, x2);
Здесь клиент сам выполняет приведение данных к типу object и обратно, так что все работает нормально. Более того, можно, используя процедуру обмена, выполнить обмен данными разного типа после их приведения к универсальному типу object. Но, если меняются значениями переменные типа int и Person, то после обмена попытка привести тип Person к типу int приведет к ошибке периода выполнения.
Console.WriteLine("Обмен данными разного типа");
obj1 = n1; obj2 = x1;
gen.SwapObject(ref obj1, ref obj2);
Console.WriteLine("Приводит к ошибке периода выполнения!");
//n1 = (int)obj1; x1 = (double)obj2;
Так что необходим более мощный механизм, позволяющий справляться с возможностью хранения и работы в классе с данными разного типа.
Для достижения универсальности процедуры Swap следует рассматривать тип T как ее параметр, такой же, как и сами аргументы x1 и x2. Суть универсальности в том, чтобы в момент вызова процедуры передавать ей не только фактические аргументы, но и их фактический тип. Вот как можно в C# объявить процедуру Swap с параметром, задающим тип аргументов:
public void Swap<T>(ref T item1, ref T item2)
{
T temp = item1;
item1 = item2; item2 = temp;
}
Вот как клиент может вызывать этот метод для данных разных типов:
public void TestSwapT()
{
int n1 = 7, n2 = 11;
double x1 = 7.7, x2 = 11.1;
Console.WriteLine("n1 = {0}, n2 = {1}", n1, n2);
Console.WriteLine("x1 = {0}, x2 = {1}", x1, x2);
Console.WriteLine("После обмена данными одного типа");
gen.Swap<int>(ref n1, ref n2);
gen.Swap<double>(ref x1, ref x2);
Console.WriteLine("n1 = {0}, n2 = {1}", n1, n2);
Console.WriteLine("x1 = {0}, x2 = {1}", x1, x2);
Заметьте, в момент вызова метода ему передаются как объекты, подлежащие обмену, так и их тип, одинаковый для обоих объектов. Что произойдет, если попытаться обменять объекты разных типов?
Console.WriteLine("Попытка обмена данными разного типа");
//gen.Swap<int>(ref n1, ref x1);
//gen.Swap<double>(ref x1, ref n1);
Console.WriteLine("заканчивается ошибкой еще на этапе компиляции!");
Ошибка, естественно, возникнет. Но! Это ошибка периода компиляции, а не выполнения, она уведомляет разработчика, что нельзя так просто смешивать "королей" и "капусту". Так что механизм работает должным образом. Рассмотрим, как универсальность распространяется на класс.
Под универсальностью (genericity) понимается способность класса объявлять используемые им типы как параметры. Класс с параметрами, задающими типы, называется универсальным классом (generic class). Терминология не устоялась, и синонимами термина "
Объявить класс C# универсальным просто: для этого достаточно указать в объявлении класса, какие из используемых им типов являются параметрами. Список типовых параметров класса, заключенный в угловые скобки, добавляется к имени класса:
class MyClass<T1, … Tn> {…}
Как и всякие формальные параметры, Ti являются именами (идентификаторами). В теле класса эти имена могут задавать типы некоторых полей класса, локальных переменных, типы аргументов и возвращаемых значений методов класса. В некоторый момент (об этом скажем чуть позже) формальные имена типов будут заменены фактическими параметрами, представляющими уже конкретные типы - имена
В C# универсальными могут быть как классы, так и все их частные случаи - интерфейсы, структуры, делегаты, события.
Специальным частным случаем Generic является примером такого класса:
class Generic
{
public void Swap<T>(ref T item1, ref T item2)
{
T temp = item1;
item1 = item2; item2 = temp;
}
public void SwapObject( ref object item1, ref object item2)
{
object temp = item1;
item1 = item2; item2 = temp;
}
}
Как видите, сам класс в данном случае не имеет родовых параметров, но зато универсальным является один из методов класса - Swap, имеющий родовой параметр T. Типу Т принадлежат аргументы метода и локальная переменная temp. Всякий раз при вызове метода ему, наряду с T в описании метода. О некоторых деталях технологии подстановки и выполнения метода поговорим в конце лекции, сейчас же лишь отмечу, что реализация вызова универсального метода в C# не приводит к существенным накладным расходам.
Наследование и универсальность являются двумя основными механизмами, обеспечивающими мощность объектной технологии разработки. Наследование позволяет специализировать
Эти механизмы взаимно дополняют друг друга. Универсальность можно ограничить (об этом подробнее будет сказано ниже), указав, что тип, задаваемый родовым параметром, обязан быть наследником некоторого класса и/или ряда интерфейсов. С другой стороны, когда формальный тип T заменяется фактическим типом TFact, то там, где разрешено появляться объектам типа TFact, разрешены и объекты, принадлежащие классам-потомкам TFact.
Эти механизмы в совокупности обеспечивают бесшовный процесс разработки программных систем, начиная с этапов спецификации и проектирования системы и заканчивая этапами реализации и сопровождения. На этапе задания спецификаций появляются абстрактные,
(рис 8.1) Этапы процесса разработки программной системыНа этапе спецификации, как правило, создается абстрактный,
Для наполнения этой схемы реальным содержанием давайте рассмотрим некоторый пример с прохождением всех трех этапов.
Возьмем классическую задачу определения стека. Следуя схеме, определим абстрактный
abstract public class GenStack<T>
{
abstract public T item();
abstract public void remove();
abstract public void put(T t);
abstract public bool empty();
}
В таком виде абстрактный класс задает summary, играющие три важные роли. С одной стороны - это комментарии, важные для разработчика проекта. На этапе сопровождения проекта значение этой роли только усиливается. Теги summary появляются как интеллектуальная подсказка у клиентов класса при работе с классом и его методами, - в этом их вторая роль. Наконец, третья роль - они позволяют автоматическое построение документации -
Вот как выглядит наш абстрактный класс, дополненный подробными спецификациями:
/// <summary>
/// Абстрактный класс GenStack{T} задает контейнер с доступом LIFO:
/// Функции:
/// конструктор new: -> GenStack{T}
/// запросы:
/// item: GenStack -> T
/// empty: GenStack -> Boolean
/// процедуры:
/// put: GenStack*T -> GenStack
/// remove: GenStack -> GenStack
/// Аксиомы:
/// remove(put(s,x)) = s
/// item(put(s,x)) = x
/// empty(new)= true
/// empty(put(s,x)) = false
/// </summary>
abstract public class GenStack<T>
{
/// <summary>
/// require: not empty();
/// </summary>
/// <returns>элемент вершины(последний пришедший)</returns>
abstract public T item();
/// <summary>
/// require: not empty();
/// ensure: удален элемент вершины(последний пришедший)
/// </summary>
abstract public void remove();
/// <summary>
/// require: true; ensure: elem находится в вершине стека
/// </summary>
/// <param name="elem"></param>
abstract public void put(T elem);
/// <summary>
/// require: true;
/// </summary>
/// <returns>true если стек пуст, иначе false </returns>
abstract public bool empty();
}// class GenStack
В приведенном примере программного текста - чуть-чуть. Основной текст задает описание спецификации класса и его методов. Заметьте, здесь спецификации заданы достаточно формально с использованием аксиом, характеризующих смысл операций, которые выполняются над стеком. Не хочется вдаваться в математические подробности, отмечу лишь, что, если задать последовательность операций над стеком, то аксиомы позволяют точно определить состояние стека в результате выполнения этих операций. Как неоднократно отмечалось с первых лекций курса, XML-отчет, построенный по этому проекту, будет содержать в читаемой форме все спецификации нашего класса. Отмечу еще, что все потомки класса должны удовлетворять этим спецификациям, хотя могут добавлять и собственные ограничения. К сожалению, потомки не наследуют текста, заданного в комментариях, - в тегах summary и других тегах, так что для потомков приходится повторно задавать теги summary.
Наш класс определен как
Наш класс определен как абстрактный класс - не задана ни реализация методов, ни то, как стек будет представлен. Эти вопросы будут решать потомки класса.
Перейдем теперь ко второму этапу и построим потомков класса, каждый из которых задает некоторое представление стека и соответствующую этому
Вот как выглядит первый потомок абстрактного класса:
/// <summary>
/// Стек, построенный на односвязных элементах списка GenLinkable{T}
/// </summary>
public class OneLinkStack<T> : GenStack<T>
{
/// <summary>
/// ссылка на стек (вершину стека)
/// </summary>
GenLinkable<T> last;
/// <summary>
/// Конструктор без аргументов
/// </summary>
public OneLinkStack()
{
last = null;
}
/**
* <remarks>Реализация методов абстрактного класса</remarks>
* */
/// <summary>
/// require: not empty();
/// </summary>
/// <returns>элемент вершины(последний пришедший)</returns>
public override T item()
{
return (last.Item);
}//item
/// <summary>
/// require: true;
/// </summary>
/// <returns>true если стек пуст, иначе false </returns>
public override bool empty()
{
return (last == null);
}//empty
/// <summary>
/// require: true; ensure: elem находится в вершине стека
/// </summary>
/// <param name="elem"></param>
public override void put(T elem)
{
GenLinkable<T> newitem = new GenLinkable<T>();
newitem.Item = elem; newitem.Next = last;
last = newitem;
}//put
/// <summary>
/// require: not empty();
/// ensure: удален элемент вершины(последний пришедший)
/// </summary>
public override void remove()
{
last = last.Next;
}//remove
}//class OneLinkStack
Посмотрите, что происходит при наследовании от
public class OneLinkStack<T> : GenStack<T>
Во-вторых, если потомок является клиентом некоторого класса, то и этот класс, возможно, также должен быть универсальным, как в нашем случае происходит с классом GenLinkable<T>:
GenLinkable<T> last; //ссылка на стек (элемент стека)
В-третьих, тип T встречается в тексте потомка всюду, где речь идет о типе элементов, добавляемых в стек, как, например:
public override void put(T elem)
По ходу дела нам понадобился класс, задающий представление элементов стека в списковом представлении. Объявим его:
/// <summary>
/// Элемент односвязного списка
/// содержит два поля - информационное и ссылку
/// </summary>
/// <typeparam name="T">тип элементов, хранимых в списке</typeparam>
public class GenLinkable<T>
{
/// <summary>
/// Информационное поле - объект типа T
/// </summary>
public T Item;
/// <summary>
/// Ссылка на следующий элемент списка
/// </summary>
public GenLinkable<T> Next;
/// <summary>
/// Конструктор без аргументов
/// Инициализиует поля значениями по умолчанию
/// </summary>
public GenLinkable()
{ Item = default(T); Next = null; }
}
Класс устроен достаточно просто: у него два поля, одно для хранения элементов, помещаемых в стек и имеющее тип T, другое - указатель на следующий элемент. Обратите внимание на конструктор класса, в котором для инициализации элемента используется конструкция default(T), возвращающая значение, устанавливаемое по умолчанию для типа T.
Второй потомок абстрактного класса реализует стек по-другому, используя представление в виде массива. Потомок задает стек ограниченной емкости. Емкостью стека можно управлять в момент его создания. В ряде ситуаций применение такого стека предпочтительнее по соображениям эффективности, поскольку не требует динамического создания элементов. Приведу текст этого класса уже без дополнительных комментариев:
public class ArrayUpStack<T> : GenStack<T>
{
int SizeOfStack;
T[] stack;
int top;
/// <summary>
/// конструктор
/// </summary>
/// <param name="size">размер стека</param>
public ArrayUpStack(int size)
{ SizeOfStack = size; stack = new T[SizeOfStack]; top = 0; }
/// <summary>
/// require: (top < SizeOfStack)
/// </summary>
/// <param name="x"> элемент, помещаемый в стек</param>
public override void put(T x)
{ stack[top] = x; top++; }
public override void remove()
{ top--; }
public override T item()
{ return (stack[top-1]); }
public override bool empty()
{ return (top == 0); }
}//class ArrayUpStack
Созданные в результате наследования классы-потомки перестали быть абстрактными, но все еще остаются универсальными. На третьем этапе порождаются конкретные экземпляры потомков -
public void TestStacks()
{
OneLinkStack<int> stack1 = new OneLinkStack<int>();
OneLinkStack<string> stack2 = new OneLinkStack<string>();
ArrayUpStack<double> stack3 = new ArrayUpStack<double>(10);
stack1.put(11); stack1.put(22);
int x1 = stack1.item(), x2 = stack1.item();
if ((x1 == x2) (x1 == 22)) Console.WriteLine("OK!");
stack1.remove(); x2 = stack1.item();
if ((x1 != x2) (x2 == 11)) Console.WriteLine("OK!");
stack1.remove(); x2 = (stack1.empty())? 77 : stack1.item();
if ((x1 != x2) (x2 == 77)) Console.WriteLine("OK!");
stack2.put("first"); stack2.put("second");
stack2.remove(); string s = stack2.item();
if (!stack2.empty()) Console.WriteLine(s);
stack3.put(3.33); stack3.put(Math.Sqrt(Math.PI));
double res = stack3.item();
stack3.remove(); res += stack3.item();
Console.WriteLine("res= {0}", res);
}
В трех первых строках этой процедуры порождаются три экземпляра стеков. Все они имеют общего родителя - абстрактный GenStack, но каждый из них работает с данными своего типа и по-разному реализует методы родителя. На рис. 8.2 показаны результаты работы этой процедуры.
(рис 8.2) Три стека, порожденных универсальным классомДополним наше рассмотрение еще одним примером работы с вариацией стеков, в том числе - хранящим объекты класса 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!");
}
Результаты работы этой процедуры приведены на рис. 8.3.
(рис 8.3) Работа со стеком Person
Хорошо, когда есть свобода. Еще лучше, когда свобода ограничена. Аналогичная ситуация существует и с универсальностью. Универсальность следует ограничивать. На типы
Если немного подумать, то это совершенно естественная ситуация. Когда имеется неограниченная универсальность, над объектами типов можно выполнять только те операции, которые допускают все типы, - в C# это object, прародителя всех типов. В нашем предыдущем примере, где речь шла о
В языке C# допускаются три вида ограничений накладываемых на родовые параметры.
T является наследником некоторого класса и ряда интерфейсов. Следовательно, над объектами типа T можно выполнять все операции, заданные базовым классом и интерфейсами. Эти операции where T: BaseClass, I1, …Ik.T имеет конструктор без аргументов и, следовательно, позволяет создавать объекты типа T. Синтаксически ограничение выглядит так: where T: new().T. Для указания struct, для ссылочных - class. Так что синтаксически этот тип ограничений выглядит так: where T: struct.Возникает законный вопрос: насколько полна предлагаемая система ограничений? Конечно, речь идет о практической полноте, а не о математически строгих определениях. С позиций практики систему хотелось бы дополнить, в первую очередь, введением ограничений операций, указывающим допустимые знаки операций в выражениях над объектами соответствующего типа. Хотелось бы, например, указать, что к объектам типа T применима операция сложения + или операция сравнения <. Позже я покажу, как можно справиться с этой проблемой, но предлагаемое решение довольно сложно. Наличие ограничения, допускающего операции, намного элегантнее решало бы эту проблему.
Уточним некоторые синтаксические правила записи ограничений. Если задан 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, так же как и методы интерфейса . Можно создавать объекты типа T1, используя конструктор по умолчанию. T2, должен быть значимым, и объекты этого типа разрешается сравнивать между собой.
Ключевые идеи ограниченной универсальности, надеюсь, понятны. Давайте теперь рассмотрим пример построения подобного класса, где можно будет увидеть все детали. Возьмем классическую и саму по себе интересную задачу построения списка с курсором. Как и всякий контейнер данных, список следует сделать универсальным, допускающим хранение данных разного типа. С другой стороны, мы не хотим, чтобы в одном списке происходило смешение типов, - уж если там хранятся персоны, то чисел int в нем не должно быть. По этим причинам класс должен быть универсальным, имея в качестве параметра тип T, задающий тип хранимых данных. Мы потребуем также, чтобы данные хранились с их ключами. И поскольку не хочется заранее накладывать ограничения на тип ключей - они могут быть строковыми или числовыми, - тип хранимых ключей будет еще одним параметром нашего класса. Мы хотим определить над списком операцию поиска по ключу, значит, нам придется выполнять проверку ключей на равенство, поэтому универсальность
типа ключей должна быть ограниченной, проще всего сделать этот тип наследником стандартного интерфейса IComparable.
Чтобы не затемнять ситуацию сложностью списка, рассмотрим достаточно простой Node, два поля которого будут хранить ключ и сам элемент, а третье поле будет задавать указатель на следующий элемент списка. Очевидно, что этот класс должен быть
public 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>
{
protected Node<K, T> first, cursor;
}
Являясь клиентом Node, наш класс сохраняет родовые параметры клиента и ограничения, накладываемые на них. Два поля класса - first и cursor - задают указатели на первый и текущий элементы списка. Операции над списком связываются с курсором, позволяя перемещать курсор по списку. Рассмотрим вначале набор операций, перемещающих курсор:
public void start()
{ cursor = first; }
public void forth()
{ if (cursor.next != null) cursor = cursor.next; }
Операция start передвигает курсор к началу списка, а 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("Иоанн - найдено!");
else Console.WriteLine("Иоанн - не найдено!");
Person pers3 = new Person("Иванов", 33, 3000);
list2.add("Иоанн", pers3); list2.start();
Person pers = list2.Item(); pers.PrintPerson();
list2.findstart("Иоанн"); pers = list2.Item(); pers.PrintPerson();
}
Обратите внимание на строки, где создаются два списка:
OneLinkList<int, string> list1 = new OneLinkList<int, string>(); OneLinkList<string, Person> list2 = new OneLinkList< string, Person>();
У списка list1 ключи имеют тип int, у списка list2 - string. Заметьте, оба фактических типа, согласно обязательствам, реализуют интерфейс IComparable. У первого списка тип элементов - string, у второго - Person. Все работает прекрасно. Вот результаты вычислений по этой процедуре.
(рис 8.4) Поиск в списке с ограниченной универсальностью
Представьте себе, что мы хотим иметь специализированный вариант нашего списка, элементы которого допускали бы операцию сложения, и одно из полей сохраняло бы сумму всех элементов, добавленных в список. Как задать соответствующее ограничение на класс?
Как уже говорилось, наличие ограничения операции, где можно было бы указать, что над элементами определена операция +, решало бы проблему. Но такого INumeric, аналогичного IComparable, определяющего метод сложения Add. Так что нам не может помочь и
Вот один из возможных выходов, предлагаемых в такой ситуации. Стратегия следующая: определим абстрактный Calc с методами, выполняющими вычисления. Затем создадим конкретизированных потомков этого класса. В классе, задающем список с суммированием, введем поле класса Calc. При создании экземпляров класса будем передавать Calc:
public abstract class Calc<T>
{
public abstract T Add(T a, T b);
public abstract T Sub(T a, T b);
public abstract T Mult(T a, T b);
public abstract T Div(T a, T b);
}
Наш абстрактный
public class IntCalc : Calc<int>
{
public override int Add(int a, int b) { return (a + b); }
public override int Sub(int a, int b) { return (a - b); }
public override int Mult(int a, int b) { return (a * b); }
public override int Div(int a, int b) { return (a / b); }
}
public class DoubleCalc : Calc<double>
{
public override double Add(double a, double b) { return (a + b); }
public override double Sub(double a, double b) { return (a - b); }
public override double Mult(double a, double b) { return (a * b); }
public override double Div(double a, double b) { return (a / b); }
}
public class StringCalc : Calc<string>
{
public override string Add(string a, string b) { return (a + b); }
public override string Sub(string a, string b) { return (a ); }
public override string Mult(string a, string b) { return (a ); }
public override string Div(string a, string b) { return (a); }
}
Здесь определяются три разных калькулятора: один - над целочисленными данными, другой - над данными с плавающей точкой, третий - над строковыми данными. В последнем случае определена, по сути, только операция
Теперь нам нужно ввести изменения в ранее созданный класс OneLinkList. Обратите внимание на важный технологический принцип работы с объектными системами. Пусть уже есть нормально работающий класс с нормально работающими клиентами класса. Не следует изменять этот класс.Он закрыт для изменений. Используйте наследование и открывайте класс потомок, в который и вносите изменения, учитывающие добавляемую специфику класса. Принцип "Закрыт - Открыт" является одним из важнейших принципов построения программных систем в объектном стиле.
В полном соответствии с этим принципом построим класс SumList - потомок класса OneLinkList. То, что родительский класс является универсальным, ничуть не мешает строить потомка класса, сохраняющего универсальный характер родителя.
public class SumList<K, T> : OneLinkList<K, T> where K : IComparable<K>
{
Calc<T> calc;
T sum;
public SumList(Calc<T> calc)
{ this.calc = calc; sum = default(T); }
public new 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;
sum = calc.Add(sum, item);
}
else
{
newnode.next = cursor.next; cursor.next = newnode;
newnode.key = key; newnode.item = item;
sum = calc.Add(sum, item);
}
}
public T Sum()
{return (sum); }
}//SumList
У класса добавилось поле sum, задающее сумму хранимых элементов, и поле calc - калькулятор, выполняющий вычисления. Метод add, объявленный в классе с модификатором new, скрывает родительский метод add, задавая собственную реализацию этого метода. Родительский метод можно было бы определить как виртуальный, переопределив его у потомка, но я не стал трогать код родительского класса. К классу добавился еще один запрос, возвращающий значение поля sum.
Проведем теперь эксперименты с новыми вариантами списков, допускающих суммирование элементов:
public void TestSum()
{
SumList<string, int> list1 =
new SumList<string, int>(new IntCalc());
list1.add("Петр", 33); list1.add("Павел", 44);
Console.WriteLine("sum= {0}", list1.Sum());
SumList<string, double> list2 =
new SumList<string, double>(new DoubleCalc());
list2.add("Петр", 33.33); list2.add("Павел", 44.44);
Console.WriteLine("sum= {0}", list2.Sum());
SumList<string, string> list3 =
new SumList<string, string>(new StringCalc());
list3.add("Мама", " Мама мыла ");
list3.add("Маша", "Машу мылом!");
Console.WriteLine("sum= {0}", list3.Sum());
}
Обратите внимание на создание списков:
SumList<string, int> list1 = new SumList<string, int>(new IntCalc()); SumList<string, double> list2 = new SumList<string, double>(new DoubleCalc()); SumList<string, string> list3 = new SumList<string, string>(new StringCalc());
Как видите, конструктору объекта передается калькулятор, согласованный с типами данных, которые хранятся в списке. Приведу результаты вычислений, полученных при работе с этими списками.
(рис 8.5) Списки с суммированием
Прием, использованный для того, чтобы справиться с арифметикой, имеет более широкое методическое применение. Он основан на сочетании встраивания и наследования, и его полезно применять в разных ситуациях. В чем его суть. Мы хотели классу SumList добавить новые возможности - арифметику. Простым наследованием этого сделать нельзя, хотя бы потому, что у класса SumList уже есть родительский класс. Поэтому в класс SumList встроен объект абстрактного класса Calc, обладающий свойствами арифметики, - здесь работает встраивание. У класса Calc есть потомки, каждый из которых по-своему реализует арифметику. Здесь в полной мере используются возможности наследования. Конструктору объектов класса SumList передается нужный потомок класса Calc, и наш объект получает возможность работать с арифметикой.
Давайте посмотрим, как этот же прием позволяет справляться с проблемой множественного наследования, обходя ограничение одного родителя. На практике часто возникает необходимость построить класс, который был бы потомком двух родителей. Пусть, например, созданы классы " дом " и " автомобиль ", а хочется построить класс " домомобиль ", объединяющий свойства уже построенных классов. Аналогично, классы " дом " и " вагон " могут порождать класс " спальный вагон ", а классы " самолет " и " пароход " - " гидросамолет ".
Вот схема возможного решения этой проблемы:
public class Home
{
// описывает свойства дома
}
public abstract class Transport
{
// описывает свойства транспортных средств
}
public class Car : Transport
{
// описывает свойства автомобиля
}
public class Carriage : Transport
{
// описывает свойства вагона
}
public class Spaceship : Transport
{
// описывает свойства космического корабля
}
Имея этот набор классов, нетрудно определить новые классы, объединяющие свойства существующих классов:
public class Home_Car : Home
{
Transport transport;
public Home_Car(Transport transport)
{
this.transport = transport;
}
}
public class Home_Carriage : Home
{
Transport transport;
public Home_Carriage(Transport transport)
{
this.transport = transport;
}
}
public class Home_Spaceship
{
Transport transport;
public Home_Spaceship(Transport transport)
{
this.transport = transport;
}
}
Если теперь некоторый клиент захочет создать объект, моделирующий космический корабль и предназначенный для межзвездных путешествий, то он может вначале создать объект класса Spaceship и передать его конструктору, создающему объект класса Home_Spaceship.
public void TestSpace()
{
Spaceship spaceship = new Spaceship();
Home_Spaceship home_ship =
new Home_Spaceship(spaceship);
Console.WriteLine("И корабль плывет!");
}
До сих пор рассматривалась ситуация using, назначение которого и состоит в выполнении подобных подстановок. Предложение using не создает реальный класс. Это лишь форма сокращения записи, но содержательно его можно рассматривать как объявление конкретного класса.
Давайте вернемся к универсальному классу OneLinkStack<T>, введенному в начале этой лекции, и объявим класс IntStack, заменив формальный параметр T фактическим - int. Для этого достаточно задать следующее предложение using:
using IntStack = ConsoleGeneric.OneLinkStack<int>;
Вот тест, в котором создаются несколько объектов этого класса:
public void TestIntStack()
{
IntStack stack1 = new IntStack();
IntStack stack2 = new IntStack();
IntStack stack3 = new IntStack();
stack1.put(11); stack1.put(22);
int x1 = stack1.item(), x2 = stack1.item();
if ((x1 == x2) (x1 == 22)) Console.WriteLine("OK!");
stack1.remove(); x2 = stack1.item();
if ((x1 != x2) (x2 == 11)) Console.WriteLine("OK!");
stack1.remove(); x2 = (stack1.empty()) ? 77 : stack1.item();
if ((x1 != x2) (x2 == 77)) Console.WriteLine("OK!");
stack2.put(55); stack2.put(66);
stack2.remove(); int s = stack2.item();
if (!stack2.empty()) Console.WriteLine(s);
stack3.put(333); stack3.put((int)Math.Sqrt(Math.PI));
int res = stack3.item();
stack3.remove(); res += stack3.item();
Console.WriteLine("res= {0}", res);
}
Все работает заданным образом, можете поверить.
Универсальность - это механизм, воздействующий на все элементы языка. Поэтому он применим ко всем частным случаям классов C# .
Так же, как и обычный класс, структура может иметь родовые параметры.
public struct Point<T>
{
T x, y;//координаты точки, тип которых задан параметром
// другие свойства и методы структуры
}
Интерфейсы чаще всего следует делать универсальными, предоставляя большую гибкость для позднейших этапов создания системы. Возможно, вы заметили применение в наших примерах IComparable<T> и других. Введение универсальности, в первую очередь, сказалось на object, выполнения операций boxing и .
Делегаты также могут иметь родовые параметры. Чаще встречается ситуация, когда делегат объявляется в UniversalDelegates, в котором объявляется функциональный тип:
class UniversalDelegates<T>
{
public delegate T Unidel(T a, T b);
}
Как видите, тип аргументов и возвращаемого значения в сигнатуре функционального типа определяется параметром класса Delegate.
Добавим в класс FunAr, одним из аргументов которой будет функция типа Unidel, заданного делегатом. Эта функция будет применяться к элементам массива, передаваемого также функции FunAr. Приведу описание:
public T FunAr(T[] arr, T a0, Unidel f)
{
T temp = a0;
for(int i =0; i<arr.Length; i++)
{
temp = f(temp, arr[i]);
}
return (temp);
}
Эта
Рассмотрим теперь клиентский класс Testing, в котором определен набор функций:
public int max2(int a, int b)
{ return (a > b) ? a : b; }
public double min2(double a, double b)
{ return (a < b) ? a : b; }
public string sum2(string a, string b)
{ return a + b; }
public float prod2(float a, float b)
{ return a * b; }
Хотя все функции имеют разные типы, все они соответствуют определению класса Unidel - имеют два аргумента одного типа и возвращают результат того же типа. Посмотрим, как они применяются в тестирующем методе класса Testing:
public void TestFun()
{
int[] ar1 = { 3, 5, 7, 9 };
double[] ar2 = { 3.5, 5.7, 7.9 };
string[] ar3 = { "Мама ", "мыла ", "Машу ", "мылом." };
float[] ar4 = { 5f, 7f, 9f, 11f };
UniversalDelegates<int> d1 =
new UniversalDelegates<int>();
UniversalDelegates<int>.Unidel del1;
del1 = this.max2;
int max = d1.FunAr(ar1, ar1[0], del1);
Console.WriteLine("max= {0}", max);
UniversalDelegates<double> d2 =
new UniversalDelegates<double>();
UniversalDelegates<double>.Unidel del2;
del2 = this.min2;
double min = d2.FunAr(ar2, ar2[0], del2);
Console.WriteLine("min= {0}", min);
UniversalDelegates<string> d3 =
new UniversalDelegates<string>();
UniversalDelegates<string>.Unidel del3;
del3 = this.sum2;
string sum = d3.FunAr(ar3, "", del3);
Console.WriteLine("concat= {0}", sum);
UniversalDelegates<float> d4 =
new UniversalDelegates<float>();
UniversalDelegates<float>.Unidel del4;
del4 = this.prod2;
float prod = d4.FunAr(ar4, 1f, del4);
Console.WriteLine("prod= {0}", prod);
}
Обратите внимание на объявление
UniversalDelegates<int>.Unidel del1;
В момент объявления задается
del1= this.max2;
При выполнении этого присваивания проверяется соответствие сигнатуры функции в правой части и
Теперь, когда в языке C# появиись анонимные функции и лямбда выражения, все можно сделать значительно проще - функций не объявлять явно, не объявлять делегаты. Вот как выглядит эквивалентное решение тестового примера:
public void TestLanbdaFun()
{
int[] ar1 = { 3, 5, 7, 9 };
double[] ar2 = { 3.5, 5.7, 7.9 };
string[] ar3 =
{ "Мама ", "мыла ", "Машу ", "мылом." };
float[] ar4 = { 5f, 7f, 9f, 11f };
UniversalDelegates<int> d1 =
new UniversalDelegates<int>();
int max = d1.FunAr(ar1, ar1[0],
(int a, int b)=> { return (a > b) ? a : b; });
Console.WriteLine("max= {0}", max);
UniversalDelegates<double> d2 =
new UniversalDelegates<double>();
double min = d2.FunAr(ar2, ar2[0],
(double a, double b)=> {return (a < b) ? a : b;});
Console.WriteLine("min= {0}", min);
UniversalDelegates<string> d3 =
new UniversalDelegates<string>();
string sum = d3.FunAr(ar3, "",
(string a, string b)=> {return a + b; });
Console.WriteLine("concat= {0}", sum);
UniversalDelegates<float> d4 =
new UniversalDelegates<float>();
float prod = d4.FunAr(ar4, 1f,
(float a, float b) => {return a*b;});
Console.WriteLine("prod= {0}", prod);
}
Покажем, что и сам функциональный тип - делегат - можно объявлять с родовыми параметрами. Вот пример такого объявления:
public delegate T1 FunOneArg<T1>(T1 a);
Добавим в наш тестовый пример код, демонстрирующий работу с этим делегатом:
FunOneArg<double> F = (double x) =>
{return Math.Sin(x) + Math.Cos(x);};
Console.WriteLine("x = {0}, Sin(x) + Cos(x) = {1}",
5.0, F(5.0));
Вот как выглядят результаты работы тестового примера.
(рис 8.6) Результаты работы с универсальными делегатамиEventHandler, применяемый для всех событий, которые не имеют собственных аргументов, теперь дополнен универсальным аналогом, определенным следующим образом:
public void delegate EventHandler<T> (object sender, T args)
where T:EventArgs
Этот делегат может применяться и для событий с собственными аргументами, поскольку вместо параметра T может быть подставлен конкретный тип - потомок класса EventArgs, дополненный нужными аргументами.
Универсальность принадлежит к основным механизмам языка. Ее введение в язык C# не могло не сказаться на всех его основных свойствах. Как уже говорилось, классы и все частные случаи стали обладать этим свойством. Введение универсальности не должно было ухудшить уже достигнутые свойства языка -
Решение этих задач потребовало введения универсальности не только в язык C#, но и поддержки на уровне каркаса Framework .Net и языка IL, включающем теперь параметризованные типы.
При этом дублирования кода не происходит и на уровне JIT-компиляторов, которые, однажды сгенерировав код для конкретного типа, сохраняют ссылку на этот участок кода и передают ее, когда такой код понадобится вторично. Это справедливо как для ссылочных, так и для
Естественно, что универсальность потребовала введения в
Так, например, в класс System.Array добавлен ряд универсальных статических методов. Вот один из них:
public static int BinarySearch<T>(T[] array, T value);
В табл. 8.1 показаны некоторые System.Collections.Generic и их аналоги из пространства System.Collections.
| Универсальный класс | Обычный класс | Универсальный интерфейс | Обычный интерфейс |
|---|---|---|---|
Comparer<T> |
Comparer |
|
|
Dictionary<K,T> |
HashTable |
IComparable<T> |
IComparable |
LinkedList<T> |
---- | IDictionary<K,T> |
IDictionary |
List<T> |
ArrayList |
|
|
Queue<T> |
Queue |
IEnumerator<T> |
IEnumerator |
SortedDictionary<K,T> |
SortedList |
IList<T> |
IList |
Stack<T> |
Stack |
Sorting, содержащим различные методы сортировки. Методы должны быть универсальными и позволять сортировать объекты разных типов - персоны, машины, числа, строки. Методы должны быть функциями высших порядков, допускающими сортировать данные типа T по разным критериям, например, сортировать машины по маркам, по номерам, по фамилиям владельцев. В Windows-проекте постройте классы Person, Car и другие классы, демонстрирующие работу с классом Sorting. В клиентском классе предусмотрите построение набора функций, позволяющих сравнивать персон по разным критериям. Предусмотрите возможность задания таких функций лямбда-выражениями. В интерфейсе проекта предусмотрите возможность сравнения методов сортировки по времени. Постройте метод, вычисляющий время сортировки, как Stack<T>, Queue<T>, List<T>. Постройте Windows-проект для работы с этими классами.List<T>, LinkedList<T>. Постройте Windows-проект для работы с этими классами.Dictionary<K, T>, SortedDictionary<K, T>. Постройте Windows-проект для работы с этими классами.BinaryTreeSearch<K, T> - бинарное дерево поиска. Элементы, хранимые в дереве, обладают ключом типа K и информационным полем типа T. Ключи элементов уникальны. Для дерева поиска справедливо свойство: Ключ элемента, хранимого в корне дерева, больше ключей всех элементов, хранимых в левом поддереве, и меньше ключей всех элементов, хранимых в правом поддереве. Постройте Windows-проект для работы с этим классом.Проект к данной лекции Вы можете скачать здесь.
Необходимость в универсализации возникает с первых шагов программирования. Одна из первых процедур, появляющихся при обучении программированию, - это процедура свопинга:обмен значениями двух переменных одного типа. Выглядит она примерно так:
public void Swap(ref T x1, ref T x2)
{
T temp;
temp = x1; x1 = x2; x2 = temp;
}
Если тип T - это вполне определенный тип, например, int, string или Person, то никаких проблем не существует, все совершенно прозрачно. Но как быть, если возникает необходимость обмена данными и типа int, и типа string, и типа Person? Неужели нужно писать копии этой процедуры для каждого типа? Проблема легко решается в языках, где нет
В Swap. В строго типизированном языке C#, скажете Вы, есть универсальный тип object, переменным которого можно присваивать значения любого типа, что позволяет написать следующую процедуру обмена значениями:
public void SwapObject( ref object item1, ref object item2)
{
object temp = item1;
item1 = item2; item2 = temp;
}
Эта процедура нормально компилируется, но, заметьте, ей нельзя передать аргументы, тип которых отличается от object. Дело в том, что ref object соответствует только такой же тип object в конкретный тип T должно быть явным. Так что использовать этот метод клиент может, если только сам будет выполнять приведение типа.
В примерах, приводимых ниже, как обычно, построено решение с именем Ch8. В это решение вложен консольный проект ConsoleGeneric. В проект добавлен класс Generic, содержащий методы, подлежащие исследованию, в частности метод SwapObject. В проект также добавлен класс Testing, являющийся клиентом класса Generic:
class Testing
{
Generic gen = new Generic();
//тестирующие методы
}
Рассмотрим пример тестирования работы метода SwapObject:
public void TestSwapObject()
{
int n1 = 7, n2 = 11;
double x1 = 7.7, x2 = 11.1;
Console.WriteLine("n1 = {0}, n2 = {1}", n1, n2);
Console.WriteLine("x1 = {0}, x2 = {1}", x1, x2);
Console.WriteLine("Попытка прямого обмена данными одного типа");
// gen.SwapObject(ref n1, ref n2);
// gen.SwapObject(ref x1, ref x2);
Console.WriteLine("заканчивается ошибкой еще на этапе компиляции!");
Console.WriteLine("Необходимо явное приведение к типу! ");
object obj1, obj2;
obj1 = n1; obj2 = n2;
gen.SwapObject(ref obj1, ref obj2);
n1 = (int)obj1; n2 = (int)obj2;
Console.WriteLine("n1 = {0}, n2 = {1}", n1, n2);
obj1 = x1; obj2 = x2;
gen.SwapObject(ref obj1, ref obj2);
x1 = (double)obj1; x2 = (double)obj2;
Console.WriteLine("x1 = {0}, x2 = {1}", x1, x2);
Здесь клиент сам выполняет приведение данных к типу object и обратно, так что все работает нормально. Более того, можно, используя процедуру обмена, выполнить обмен данными разного типа после их приведения к универсальному типу object. Но, если меняются значениями переменные типа int и Person, то после обмена попытка привести тип Person к типу int приведет к ошибке периода выполнения.
Console.WriteLine("Обмен данными разного типа");
obj1 = n1; obj2 = x1;
gen.SwapObject(ref obj1, ref obj2);
Console.WriteLine("Приводит к ошибке периода выполнения!");
//n1 = (int)obj1; x1 = (double)obj2;
Так что необходим более мощный механизм, позволяющий справляться с возможностью хранения и работы в классе с данными разного типа.
Для достижения универсальности процедуры Swap следует рассматривать тип T как ее параметр, такой же, как и сами аргументы x1 и x2. Суть универсальности в том, чтобы в момент вызова процедуры передавать ей не только фактические аргументы, но и их фактический тип. Вот как можно в C# объявить процедуру Swap с параметром, задающим тип аргументов:
public void Swap<T>(ref T item1, ref T item2)
{
T temp = item1;
item1 = item2; item2 = temp;
}
Вот как клиент может вызывать этот метод для данных разных типов:
public void TestSwapT()
{
int n1 = 7, n2 = 11;
double x1 = 7.7, x2 = 11.1;
Console.WriteLine("n1 = {0}, n2 = {1}", n1, n2);
Console.WriteLine("x1 = {0}, x2 = {1}", x1, x2);
Console.WriteLine("После обмена данными одного типа");
gen.Swap<int>(ref n1, ref n2);
gen.Swap<double>(ref x1, ref x2);
Console.WriteLine("n1 = {0}, n2 = {1}", n1, n2);
Console.WriteLine("x1 = {0}, x2 = {1}", x1, x2);
Заметьте, в момент вызова метода ему передаются как объекты, подлежащие обмену, так и их тип, одинаковый для обоих объектов. Что произойдет, если попытаться обменять объекты разных типов?
Console.WriteLine("Попытка обмена данными разного типа");
//gen.Swap<int>(ref n1, ref x1);
//gen.Swap<double>(ref x1, ref n1);
Console.WriteLine("заканчивается ошибкой еще на этапе компиляции!");
Ошибка, естественно, возникнет. Но! Это ошибка периода компиляции, а не выполнения, она уведомляет разработчика, что нельзя так просто смешивать "королей" и "капусту". Так что механизм работает должным образом. Рассмотрим, как универсальность распространяется на класс.
Под универсальностью (genericity) понимается способность класса объявлять используемые им типы как параметры. Класс с параметрами, задающими типы, называется универсальным классом (generic class). Терминология не устоялась, и синонимами термина "
Объявить класс C# универсальным просто: для этого достаточно указать в объявлении класса, какие из используемых им типов являются параметрами. Список типовых параметров класса, заключенный в угловые скобки, добавляется к имени класса:
class MyClass<T1, … Tn> {…}
Как и всякие формальные параметры, Ti являются именами (идентификаторами). В теле класса эти имена могут задавать типы некоторых полей класса, локальных переменных, типы аргументов и возвращаемых значений методов класса. В некоторый момент (об этом скажем чуть позже) формальные имена типов будут заменены фактическими параметрами, представляющими уже конкретные типы - имена
В C# универсальными могут быть как классы, так и все их частные случаи - интерфейсы, структуры, делегаты, события.
Специальным частным случаем Generic является примером такого класса:
class Generic
{
public void Swap<T>(ref T item1, ref T item2)
{
T temp = item1;
item1 = item2; item2 = temp;
}
public void SwapObject( ref object item1, ref object item2)
{
object temp = item1;
item1 = item2; item2 = temp;
}
}
Как видите, сам класс в данном случае не имеет родовых параметров, но зато универсальным является один из методов класса - Swap, имеющий родовой параметр T. Типу Т принадлежат аргументы метода и локальная переменная temp. Всякий раз при вызове метода ему, наряду с T в описании метода. О некоторых деталях технологии подстановки и выполнения метода поговорим в конце лекции, сейчас же лишь отмечу, что реализация вызова универсального метода в C# не приводит к существенным накладным расходам.
Наследование и универсальность являются двумя основными механизмами, обеспечивающими мощность объектной технологии разработки. Наследование позволяет специализировать
Эти механизмы взаимно дополняют друг друга. Универсальность можно ограничить (об этом подробнее будет сказано ниже), указав, что тип, задаваемый родовым параметром, обязан быть наследником некоторого класса и/или ряда интерфейсов. С другой стороны, когда формальный тип T заменяется фактическим типом TFact, то там, где разрешено появляться объектам типа TFact, разрешены и объекты, принадлежащие классам-потомкам TFact.
Эти механизмы в совокупности обеспечивают бесшовный процесс разработки программных систем, начиная с этапов спецификации и проектирования системы и заканчивая этапами реализации и сопровождения. На этапе задания спецификаций появляются абстрактные,
(рис 8.1) Этапы процесса разработки программной системыНа этапе спецификации, как правило, создается абстрактный,
Для наполнения этой схемы реальным содержанием давайте рассмотрим некоторый пример с прохождением всех трех этапов.
Возьмем классическую задачу определения стека. Следуя схеме, определим абстрактный
abstract public class GenStack<T>
{
abstract public T item();
abstract public void remove();
abstract public void put(T t);
abstract public bool empty();
}
В таком виде абстрактный класс задает summary, играющие три важные роли. С одной стороны - это комментарии, важные для разработчика проекта. На этапе сопровождения проекта значение этой роли только усиливается. Теги summary появляются как интеллектуальная подсказка у клиентов класса при работе с классом и его методами, - в этом их вторая роль. Наконец, третья роль - они позволяют автоматическое построение документации -
Вот как выглядит наш абстрактный класс, дополненный подробными спецификациями:
/// <summary>
/// Абстрактный класс GenStack{T} задает контейнер с доступом LIFO:
/// Функции:
/// конструктор new: -> GenStack{T}
/// запросы:
/// item: GenStack -> T
/// empty: GenStack -> Boolean
/// процедуры:
/// put: GenStack*T -> GenStack
/// remove: GenStack -> GenStack
/// Аксиомы:
/// remove(put(s,x)) = s
/// item(put(s,x)) = x
/// empty(new)= true
/// empty(put(s,x)) = false
/// </summary>
abstract public class GenStack<T>
{
/// <summary>
/// require: not empty();
/// </summary>
/// <returns>элемент вершины(последний пришедший)</returns>
abstract public T item();
/// <summary>
/// require: not empty();
/// ensure: удален элемент вершины(последний пришедший)
/// </summary>
abstract public void remove();
/// <summary>
/// require: true; ensure: elem находится в вершине стека
/// </summary>
/// <param name="elem"></param>
abstract public void put(T elem);
/// <summary>
/// require: true;
/// </summary>
/// <returns>true если стек пуст, иначе false </returns>
abstract public bool empty();
}// class GenStack
В приведенном примере программного текста - чуть-чуть. Основной текст задает описание спецификации класса и его методов. Заметьте, здесь спецификации заданы достаточно формально с использованием аксиом, характеризующих смысл операций, которые выполняются над стеком. Не хочется вдаваться в математические подробности, отмечу лишь, что, если задать последовательность операций над стеком, то аксиомы позволяют точно определить состояние стека в результате выполнения этих операций. Как неоднократно отмечалось с первых лекций курса, XML-отчет, построенный по этому проекту, будет содержать в читаемой форме все спецификации нашего класса. Отмечу еще, что все потомки класса должны удовлетворять этим спецификациям, хотя могут добавлять и собственные ограничения. К сожалению, потомки не наследуют текста, заданного в комментариях, - в тегах summary и других тегах, так что для потомков приходится повторно задавать теги summary.
Наш класс определен как
Наш класс определен как абстрактный класс - не задана ни реализация методов, ни то, как стек будет представлен. Эти вопросы будут решать потомки класса.
Перейдем теперь ко второму этапу и построим потомков класса, каждый из которых задает некоторое представление стека и соответствующую этому
Вот как выглядит первый потомок абстрактного класса:
/// <summary>
/// Стек, построенный на односвязных элементах списка GenLinkable{T}
/// </summary>
public class OneLinkStack<T> : GenStack<T>
{
/// <summary>
/// ссылка на стек (вершину стека)
/// </summary>
GenLinkable<T> last;
/// <summary>
/// Конструктор без аргументов
/// </summary>
public OneLinkStack()
{
last = null;
}
/**
* <remarks>Реализация методов абстрактного класса</remarks>
* */
/// <summary>
/// require: not empty();
/// </summary>
/// <returns>элемент вершины(последний пришедший)</returns>
public override T item()
{
return (last.Item);
}//item
/// <summary>
/// require: true;
/// </summary>
/// <returns>true если стек пуст, иначе false </returns>
public override bool empty()
{
return (last == null);
}//empty
/// <summary>
/// require: true; ensure: elem находится в вершине стека
/// </summary>
/// <param name="elem"></param>
public override void put(T elem)
{
GenLinkable<T> newitem = new GenLinkable<T>();
newitem.Item = elem; newitem.Next = last;
last = newitem;
}//put
/// <summary>
/// require: not empty();
/// ensure: удален элемент вершины(последний пришедший)
/// </summary>
public override void remove()
{
last = last.Next;
}//remove
}//class OneLinkStack
Посмотрите, что происходит при наследовании от
public class OneLinkStack<T> : GenStack<T>
Во-вторых, если потомок является клиентом некоторого класса, то и этот класс, возможно, также должен быть универсальным, как в нашем случае происходит с классом GenLinkable<T>:
GenLinkable<T> last; //ссылка на стек (элемент стека)
В-третьих, тип T встречается в тексте потомка всюду, где речь идет о типе элементов, добавляемых в стек, как, например:
public override void put(T elem)
По ходу дела нам понадобился класс, задающий представление элементов стека в списковом представлении. Объявим его:
/// <summary>
/// Элемент односвязного списка
/// содержит два поля - информационное и ссылку
/// </summary>
/// <typeparam name="T">тип элементов, хранимых в списке</typeparam>
public class GenLinkable<T>
{
/// <summary>
/// Информационное поле - объект типа T
/// </summary>
public T Item;
/// <summary>
/// Ссылка на следующий элемент списка
/// </summary>
public GenLinkable<T> Next;
/// <summary>
/// Конструктор без аргументов
/// Инициализиует поля значениями по умолчанию
/// </summary>
public GenLinkable()
{ Item = default(T); Next = null; }
}
Класс устроен достаточно просто: у него два поля, одно для хранения элементов, помещаемых в стек и имеющее тип T, другое - указатель на следующий элемент. Обратите внимание на конструктор класса, в котором для инициализации элемента используется конструкция default(T), возвращающая значение, устанавливаемое по умолчанию для типа T.
Второй потомок абстрактного класса реализует стек по-другому, используя представление в виде массива. Потомок задает стек ограниченной емкости. Емкостью стека можно управлять в момент его создания. В ряде ситуаций применение такого стека предпочтительнее по соображениям эффективности, поскольку не требует динамического создания элементов. Приведу текст этого класса уже без дополнительных комментариев:
public class ArrayUpStack<T> : GenStack<T>
{
int SizeOfStack;
T[] stack;
int top;
/// <summary>
/// конструктор
/// </summary>
/// <param name="size">размер стека</param>
public ArrayUpStack(int size)
{ SizeOfStack = size; stack = new T[SizeOfStack]; top = 0; }
/// <summary>
/// require: (top < SizeOfStack)
/// </summary>
/// <param name="x"> элемент, помещаемый в стек</param>
public override void put(T x)
{ stack[top] = x; top++; }
public override void remove()
{ top--; }
public override T item()
{ return (stack[top-1]); }
public override bool empty()
{ return (top == 0); }
}//class ArrayUpStack
Созданные в результате наследования классы-потомки перестали быть абстрактными, но все еще остаются универсальными. На третьем этапе порождаются конкретные экземпляры потомков -
public void TestStacks()
{
OneLinkStack<int> stack1 = new OneLinkStack<int>();
OneLinkStack<string> stack2 = new OneLinkStack<string>();
ArrayUpStack<double> stack3 = new ArrayUpStack<double>(10);
stack1.put(11); stack1.put(22);
int x1 = stack1.item(), x2 = stack1.item();
if ((x1 == x2) (x1 == 22)) Console.WriteLine("OK!");
stack1.remove(); x2 = stack1.item();
if ((x1 != x2) (x2 == 11)) Console.WriteLine("OK!");
stack1.remove(); x2 = (stack1.empty())? 77 : stack1.item();
if ((x1 != x2) (x2 == 77)) Console.WriteLine("OK!");
stack2.put("first"); stack2.put("second");
stack2.remove(); string s = stack2.item();
if (!stack2.empty()) Console.WriteLine(s);
stack3.put(3.33); stack3.put(Math.Sqrt(Math.PI));
double res = stack3.item();
stack3.remove(); res += stack3.item();
Console.WriteLine("res= {0}", res);
}
В трех первых строках этой процедуры порождаются три экземпляра стеков. Все они имеют общего родителя - абстрактный GenStack, но каждый из них работает с данными своего типа и по-разному реализует методы родителя. На рис. 8.2 показаны результаты работы этой процедуры.
(рис 8.2) Три стека, порожденных универсальным классомДополним наше рассмотрение еще одним примером работы с вариацией стеков, в том числе - хранящим объекты класса 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!");
}
Результаты работы этой процедуры приведены на рис. 8.3.
(рис 8.3) Работа со стеком Person
Хорошо, когда есть свобода. Еще лучше, когда свобода ограничена. Аналогичная ситуация существует и с универсальностью. Универсальность следует ограничивать. На типы
Если немного подумать, то это совершенно естественная ситуация. Когда имеется неограниченная универсальность, над объектами типов можно выполнять только те операции, которые допускают все типы, - в C# это object, прародителя всех типов. В нашем предыдущем примере, где речь шла о
В языке C# допускаются три вида ограничений накладываемых на родовые параметры.
T является наследником некоторого класса и ряда интерфейсов. Следовательно, над объектами типа T можно выполнять все операции, заданные базовым классом и интерфейсами. Эти операции where T: BaseClass, I1, …Ik.T имеет конструктор без аргументов и, следовательно, позволяет создавать объекты типа T. Синтаксически ограничение выглядит так: where T: new().T. Для указания struct, для ссылочных - class. Так что синтаксически этот тип ограничений выглядит так: where T: struct.Возникает законный вопрос: насколько полна предлагаемая система ограничений? Конечно, речь идет о практической полноте, а не о математически строгих определениях. С позиций практики систему хотелось бы дополнить, в первую очередь, введением ограничений операций, указывающим допустимые знаки операций в выражениях над объектами соответствующего типа. Хотелось бы, например, указать, что к объектам типа T применима операция сложения + или операция сравнения <. Позже я покажу, как можно справиться с этой проблемой, но предлагаемое решение довольно сложно. Наличие ограничения, допускающего операции, намного элегантнее решало бы эту проблему.
Уточним некоторые синтаксические правила записи ограничений. Если задан 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, так же как и методы интерфейса . Можно создавать объекты типа T1, используя конструктор по умолчанию. T2, должен быть значимым, и объекты этого типа разрешается сравнивать между собой.
Ключевые идеи ограниченной универсальности, надеюсь, понятны. Давайте теперь рассмотрим пример построения подобного класса, где можно будет увидеть все детали. Возьмем классическую и саму по себе интересную задачу построения списка с курсором. Как и всякий контейнер данных, список следует сделать универсальным, допускающим хранение данных разного типа. С другой стороны, мы не хотим, чтобы в одном списке происходило смешение типов, - уж если там хранятся персоны, то чисел int в нем не должно быть. По этим причинам класс должен быть универсальным, имея в качестве параметра тип T, задающий тип хранимых данных. Мы потребуем также, чтобы данные хранились с их ключами. И поскольку не хочется заранее накладывать ограничения на тип ключей - они могут быть строковыми или числовыми, - тип хранимых ключей будет еще одним параметром нашего класса. Мы хотим определить над списком операцию поиска по ключу, значит, нам придется выполнять проверку ключей на равенство, поэтому универсальность
типа ключей должна быть ограниченной, проще всего сделать этот тип наследником стандартного интерфейса IComparable.
Чтобы не затемнять ситуацию сложностью списка, рассмотрим достаточно простой Node, два поля которого будут хранить ключ и сам элемент, а третье поле будет задавать указатель на следующий элемент списка. Очевидно, что этот класс должен быть
public 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>
{
protected Node<K, T> first, cursor;
}
Являясь клиентом Node, наш класс сохраняет родовые параметры клиента и ограничения, накладываемые на них. Два поля класса - first и cursor - задают указатели на первый и текущий элементы списка. Операции над списком связываются с курсором, позволяя перемещать курсор по списку. Рассмотрим вначале набор операций, перемещающих курсор:
public void start()
{ cursor = first; }
public void forth()
{ if (cursor.next != null) cursor = cursor.next; }
Операция start передвигает курсор к началу списка, а 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("Иоанн - найдено!");
else Console.WriteLine("Иоанн - не найдено!");
Person pers3 = new Person("Иванов", 33, 3000);
list2.add("Иоанн", pers3); list2.start();
Person pers = list2.Item(); pers.PrintPerson();
list2.findstart("Иоанн"); pers = list2.Item(); pers.PrintPerson();
}
Обратите внимание на строки, где создаются два списка:
OneLinkList<int, string> list1 = new OneLinkList<int, string>(); OneLinkList<string, Person> list2 = new OneLinkList< string, Person>();
У списка list1 ключи имеют тип int, у списка list2 - string. Заметьте, оба фактических типа, согласно обязательствам, реализуют интерфейс IComparable. У первого списка тип элементов - string, у второго - Person. Все работает прекрасно. Вот результаты вычислений по этой процедуре.
(рис 8.4) Поиск в списке с ограниченной универсальностью
Представьте себе, что мы хотим иметь специализированный вариант нашего списка, элементы которого допускали бы операцию сложения, и одно из полей сохраняло бы сумму всех элементов, добавленных в список. Как задать соответствующее ограничение на класс?
Как уже говорилось, наличие ограничения операции, где можно было бы указать, что над элементами определена операция +, решало бы проблему. Но такого INumeric, аналогичного IComparable, определяющего метод сложения Add. Так что нам не может помочь и
Вот один из возможных выходов, предлагаемых в такой ситуации. Стратегия следующая: определим абстрактный Calc с методами, выполняющими вычисления. Затем создадим конкретизированных потомков этого класса. В классе, задающем список с суммированием, введем поле класса Calc. При создании экземпляров класса будем передавать Calc:
public abstract class Calc<T>
{
public abstract T Add(T a, T b);
public abstract T Sub(T a, T b);
public abstract T Mult(T a, T b);
public abstract T Div(T a, T b);
}
Наш абстрактный
public class IntCalc : Calc<int>
{
public override int Add(int a, int b) { return (a + b); }
public override int Sub(int a, int b) { return (a - b); }
public override int Mult(int a, int b) { return (a * b); }
public override int Div(int a, int b) { return (a / b); }
}
public class DoubleCalc : Calc<double>
{
public override double Add(double a, double b) { return (a + b); }
public override double Sub(double a, double b) { return (a - b); }
public override double Mult(double a, double b) { return (a * b); }
public override double Div(double a, double b) { return (a / b); }
}
public class StringCalc : Calc<string>
{
public override string Add(string a, string b) { return (a + b); }
public override string Sub(string a, string b) { return (a ); }
public override string Mult(string a, string b) { return (a ); }
public override string Div(string a, string b) { return (a); }
}
Здесь определяются три разных калькулятора: один - над целочисленными данными, другой - над данными с плавающей точкой, третий - над строковыми данными. В последнем случае определена, по сути, только операция
Теперь нам нужно ввести изменения в ранее созданный класс OneLinkList. Обратите внимание на важный технологический принцип работы с объектными системами. Пусть уже есть нормально работающий класс с нормально работающими клиентами класса. Не следует изменять этот класс.Он закрыт для изменений. Используйте наследование и открывайте класс потомок, в который и вносите изменения, учитывающие добавляемую специфику класса. Принцип "Закрыт - Открыт" является одним из важнейших принципов построения программных систем в объектном стиле.
В полном соответствии с этим принципом построим класс SumList - потомок класса OneLinkList. То, что родительский класс является универсальным, ничуть не мешает строить потомка класса, сохраняющего универсальный характер родителя.
public class SumList<K, T> : OneLinkList<K, T> where K : IComparable<K>
{
Calc<T> calc;
T sum;
public SumList(Calc<T> calc)
{ this.calc = calc; sum = default(T); }
public new 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;
sum = calc.Add(sum, item);
}
else
{
newnode.next = cursor.next; cursor.next = newnode;
newnode.key = key; newnode.item = item;
sum = calc.Add(sum, item);
}
}
public T Sum()
{return (sum); }
}//SumList
У класса добавилось поле sum, задающее сумму хранимых элементов, и поле calc - калькулятор, выполняющий вычисления. Метод add, объявленный в классе с модификатором new, скрывает родительский метод add, задавая собственную реализацию этого метода. Родительский метод можно было бы определить как виртуальный, переопределив его у потомка, но я не стал трогать код родительского класса. К классу добавился еще один запрос, возвращающий значение поля sum.
Проведем теперь эксперименты с новыми вариантами списков, допускающих суммирование элементов:
public void TestSum()
{
SumList<string, int> list1 =
new SumList<string, int>(new IntCalc());
list1.add("Петр", 33); list1.add("Павел", 44);
Console.WriteLine("sum= {0}", list1.Sum());
SumList<string, double> list2 =
new SumList<string, double>(new DoubleCalc());
list2.add("Петр", 33.33); list2.add("Павел", 44.44);
Console.WriteLine("sum= {0}", list2.Sum());
SumList<string, string> list3 =
new SumList<string, string>(new StringCalc());
list3.add("Мама", " Мама мыла ");
list3.add("Маша", "Машу мылом!");
Console.WriteLine("sum= {0}", list3.Sum());
}
Обратите внимание на создание списков:
SumList<string, int> list1 = new SumList<string, int>(new IntCalc()); SumList<string, double> list2 = new SumList<string, double>(new DoubleCalc()); SumList<string, string> list3 = new SumList<string, string>(new StringCalc());
Как видите, конструктору объекта передается калькулятор, согласованный с типами данных, которые хранятся в списке. Приведу результаты вычислений, полученных при работе с этими списками.
(рис 8.5) Списки с суммированием
Прием, использованный для того, чтобы справиться с арифметикой, имеет более широкое методическое применение. Он основан на сочетании встраивания и наследования, и его полезно применять в разных ситуациях. В чем его суть. Мы хотели классу SumList добавить новые возможности - арифметику. Простым наследованием этого сделать нельзя, хотя бы потому, что у класса SumList уже есть родительский класс. Поэтому в класс SumList встроен объект абстрактного класса Calc, обладающий свойствами арифметики, - здесь работает встраивание. У класса Calc есть потомки, каждый из которых по-своему реализует арифметику. Здесь в полной мере используются возможности наследования. Конструктору объектов класса SumList передается нужный потомок класса Calc, и наш объект получает возможность работать с арифметикой.
Давайте посмотрим, как этот же прием позволяет справляться с проблемой множественного наследования, обходя ограничение одного родителя. На практике часто возникает необходимость построить класс, который был бы потомком двух родителей. Пусть, например, созданы классы " дом " и " автомобиль ", а хочется построить класс " домомобиль ", объединяющий свойства уже построенных классов. Аналогично, классы " дом " и " вагон " могут порождать класс " спальный вагон ", а классы " самолет " и " пароход " - " гидросамолет ".
Вот схема возможного решения этой проблемы:
public class Home
{
// описывает свойства дома
}
public abstract class Transport
{
// описывает свойства транспортных средств
}
public class Car : Transport
{
// описывает свойства автомобиля
}
public class Carriage : Transport
{
// описывает свойства вагона
}
public class Spaceship : Transport
{
// описывает свойства космического корабля
}
Имея этот набор классов, нетрудно определить новые классы, объединяющие свойства существующих классов:
public class Home_Car : Home
{
Transport transport;
public Home_Car(Transport transport)
{
this.transport = transport;
}
}
public class Home_Carriage : Home
{
Transport transport;
public Home_Carriage(Transport transport)
{
this.transport = transport;
}
}
public class Home_Spaceship
{
Transport transport;
public Home_Spaceship(Transport transport)
{
this.transport = transport;
}
}
Если теперь некоторый клиент захочет создать объект, моделирующий космический корабль и предназначенный для межзвездных путешествий, то он может вначале создать объект класса Spaceship и передать его конструктору, создающему объект класса Home_Spaceship.
public void TestSpace()
{
Spaceship spaceship = new Spaceship();
Home_Spaceship home_ship =
new Home_Spaceship(spaceship);
Console.WriteLine("И корабль плывет!");
}
До сих пор рассматривалась ситуация using, назначение которого и состоит в выполнении подобных подстановок. Предложение using не создает реальный класс. Это лишь форма сокращения записи, но содержательно его можно рассматривать как объявление конкретного класса.
Давайте вернемся к универсальному классу OneLinkStack<T>, введенному в начале этой лекции, и объявим класс IntStack, заменив формальный параметр T фактическим - int. Для этого достаточно задать следующее предложение using:
using IntStack = ConsoleGeneric.OneLinkStack<int>;
Вот тест, в котором создаются несколько объектов этого класса:
public void TestIntStack()
{
IntStack stack1 = new IntStack();
IntStack stack2 = new IntStack();
IntStack stack3 = new IntStack();
stack1.put(11); stack1.put(22);
int x1 = stack1.item(), x2 = stack1.item();
if ((x1 == x2) (x1 == 22)) Console.WriteLine("OK!");
stack1.remove(); x2 = stack1.item();
if ((x1 != x2) (x2 == 11)) Console.WriteLine("OK!");
stack1.remove(); x2 = (stack1.empty()) ? 77 : stack1.item();
if ((x1 != x2) (x2 == 77)) Console.WriteLine("OK!");
stack2.put(55); stack2.put(66);
stack2.remove(); int s = stack2.item();
if (!stack2.empty()) Console.WriteLine(s);
stack3.put(333); stack3.put((int)Math.Sqrt(Math.PI));
int res = stack3.item();
stack3.remove(); res += stack3.item();
Console.WriteLine("res= {0}", res);
}
Все работает заданным образом, можете поверить.
Универсальность - это механизм, воздействующий на все элементы языка. Поэтому он применим ко всем частным случаям классов C# .
Так же, как и обычный класс, структура может иметь родовые параметры.
public struct Point<T>
{
T x, y;//координаты точки, тип которых задан параметром
// другие свойства и методы структуры
}
Интерфейсы чаще всего следует делать универсальными, предоставляя большую гибкость для позднейших этапов создания системы. Возможно, вы заметили применение в наших примерах IComparable<T> и других. Введение универсальности, в первую очередь, сказалось на object, выполнения операций boxing и .
Делегаты также могут иметь родовые параметры. Чаще встречается ситуация, когда делегат объявляется в UniversalDelegates, в котором объявляется функциональный тип:
class UniversalDelegates<T>
{
public delegate T Unidel(T a, T b);
}
Как видите, тип аргументов и возвращаемого значения в сигнатуре функционального типа определяется параметром класса Delegate.
Добавим в класс FunAr, одним из аргументов которой будет функция типа Unidel, заданного делегатом. Эта функция будет применяться к элементам массива, передаваемого также функции FunAr. Приведу описание:
public T FunAr(T[] arr, T a0, Unidel f)
{
T temp = a0;
for(int i =0; i<arr.Length; i++)
{
temp = f(temp, arr[i]);
}
return (temp);
}
Эта
Рассмотрим теперь клиентский класс Testing, в котором определен набор функций:
public int max2(int a, int b)
{ return (a > b) ? a : b; }
public double min2(double a, double b)
{ return (a < b) ? a : b; }
public string sum2(string a, string b)
{ return a + b; }
public float prod2(float a, float b)
{ return a * b; }
Хотя все функции имеют разные типы, все они соответствуют определению класса Unidel - имеют два аргумента одного типа и возвращают результат того же типа. Посмотрим, как они применяются в тестирующем методе класса Testing:
public void TestFun()
{
int[] ar1 = { 3, 5, 7, 9 };
double[] ar2 = { 3.5, 5.7, 7.9 };
string[] ar3 = { "Мама ", "мыла ", "Машу ", "мылом." };
float[] ar4 = { 5f, 7f, 9f, 11f };
UniversalDelegates<int> d1 =
new UniversalDelegates<int>();
UniversalDelegates<int>.Unidel del1;
del1 = this.max2;
int max = d1.FunAr(ar1, ar1[0], del1);
Console.WriteLine("max= {0}", max);
UniversalDelegates<double> d2 =
new UniversalDelegates<double>();
UniversalDelegates<double>.Unidel del2;
del2 = this.min2;
double min = d2.FunAr(ar2, ar2[0], del2);
Console.WriteLine("min= {0}", min);
UniversalDelegates<string> d3 =
new UniversalDelegates<string>();
UniversalDelegates<string>.Unidel del3;
del3 = this.sum2;
string sum = d3.FunAr(ar3, "", del3);
Console.WriteLine("concat= {0}", sum);
UniversalDelegates<float> d4 =
new UniversalDelegates<float>();
UniversalDelegates<float>.Unidel del4;
del4 = this.prod2;
float prod = d4.FunAr(ar4, 1f, del4);
Console.WriteLine("prod= {0}", prod);
}
Обратите внимание на объявление
UniversalDelegates<int>.Unidel del1;
В момент объявления задается
del1= this.max2;
При выполнении этого присваивания проверяется соответствие сигнатуры функции в правой части и
Теперь, когда в языке C# появиись анонимные функции и лямбда выражения, все можно сделать значительно проще - функций не объявлять явно, не объявлять делегаты. Вот как выглядит эквивалентное решение тестового примера:
public void TestLanbdaFun()
{
int[] ar1 = { 3, 5, 7, 9 };
double[] ar2 = { 3.5, 5.7, 7.9 };
string[] ar3 =
{ "Мама ", "мыла ", "Машу ", "мылом." };
float[] ar4 = { 5f, 7f, 9f, 11f };
UniversalDelegates<int> d1 =
new UniversalDelegates<int>();
int max = d1.FunAr(ar1, ar1[0],
(int a, int b)=> { return (a > b) ? a : b; });
Console.WriteLine("max= {0}", max);
UniversalDelegates<double> d2 =
new UniversalDelegates<double>();
double min = d2.FunAr(ar2, ar2[0],
(double a, double b)=> {return (a < b) ? a : b;});
Console.WriteLine("min= {0}", min);
UniversalDelegates<string> d3 =
new UniversalDelegates<string>();
string sum = d3.FunAr(ar3, "",
(string a, string b)=> {return a + b; });
Console.WriteLine("concat= {0}", sum);
UniversalDelegates<float> d4 =
new UniversalDelegates<float>();
float prod = d4.FunAr(ar4, 1f,
(float a, float b) => {return a*b;});
Console.WriteLine("prod= {0}", prod);
}
Покажем, что и сам функциональный тип - делегат - можно объявлять с родовыми параметрами. Вот пример такого объявления:
public delegate T1 FunOneArg<T1>(T1 a);
Добавим в наш тестовый пример код, демонстрирующий работу с этим делегатом:
FunOneArg<double> F = (double x) =>
{return Math.Sin(x) + Math.Cos(x);};
Console.WriteLine("x = {0}, Sin(x) + Cos(x) = {1}",
5.0, F(5.0));
Вот как выглядят результаты работы тестового примера.
(рис 8.6) Результаты работы с универсальными делегатамиEventHandler, применяемый для всех событий, которые не имеют собственных аргументов, теперь дополнен универсальным аналогом, определенным следующим образом:
public void delegate EventHandler<T> (object sender, T args)
where T:EventArgs
Этот делегат может применяться и для событий с собственными аргументами, поскольку вместо параметра T может быть подставлен конкретный тип - потомок класса EventArgs, дополненный нужными аргументами.
Универсальность принадлежит к основным механизмам языка. Ее введение в язык C# не могло не сказаться на всех его основных свойствах. Как уже говорилось, классы и все частные случаи стали обладать этим свойством. Введение универсальности не должно было ухудшить уже достигнутые свойства языка -
Решение этих задач потребовало введения универсальности не только в язык C#, но и поддержки на уровне каркаса Framework .Net и языка IL, включающем теперь параметризованные типы.
При этом дублирования кода не происходит и на уровне JIT-компиляторов, которые, однажды сгенерировав код для конкретного типа, сохраняют ссылку на этот участок кода и передают ее, когда такой код понадобится вторично. Это справедливо как для ссылочных, так и для
Естественно, что универсальность потребовала введения в
Так, например, в класс System.Array добавлен ряд универсальных статических методов. Вот один из них:
public static int BinarySearch<T>(T[] array, T value);
В табл. 8.1 показаны некоторые System.Collections.Generic и их аналоги из пространства System.Collections.
| Универсальный класс | Обычный класс | Универсальный интерфейс | Обычный интерфейс |
|---|---|---|---|
Comparer<T> |
Comparer |
|
|
Dictionary<K,T> |
HashTable |
IComparable<T> |
IComparable |
LinkedList<T> |
---- | IDictionary<K,T> |
IDictionary |
List<T> |
ArrayList |
|
|
Queue<T> |
Queue |
IEnumerator<T> |
IEnumerator |
SortedDictionary<K,T> |
SortedList |
IList<T> |
IList |
Stack<T> |
Stack |
Sorting, содержащим различные методы сортировки. Методы должны быть универсальными и позволять сортировать объекты разных типов - персоны, машины, числа, строки. Методы должны быть функциями высших порядков, допускающими сортировать данные типа T по разным критериям, например, сортировать машины по маркам, по номерам, по фамилиям владельцев. В Windows-проекте постройте классы Person, Car и другие классы, демонстрирующие работу с классом Sorting. В клиентском классе предусмотрите построение набора функций, позволяющих сравнивать персон по разным критериям. Предусмотрите возможность задания таких функций лямбда-выражениями. В интерфейсе проекта предусмотрите возможность сравнения методов сортировки по времени. Постройте метод, вычисляющий время сортировки, как Stack<T>, Queue<T>, List<T>. Постройте Windows-проект для работы с этими классами.List<T>, LinkedList<T>. Постройте Windows-проект для работы с этими классами.Dictionary<K, T>, SortedDictionary<K, T>. Постройте Windows-проект для работы с этими классами.BinaryTreeSearch<K, T> - бинарное дерево поиска. Элементы, хранимые в дереве, обладают ключом типа K и информационным полем типа T. Ключи элементов уникальны. Для дерева поиска справедливо свойство: Ключ элемента, хранимого в корне дерева, больше ключей всех элементов, хранимых в левом поддереве, и меньше ключей всех элементов, хранимых в правом поддереве. Постройте Windows-проект для работы с этим классом.Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.