Необходимость в универсализации возникает с первых шагов программирования. Одна из первых процедур, появляющихся при обучении программированию - это процедура
public void Swap(ref T x1, ref T x2)
{
T temp;
temp = x1; x1 = x2; x2 = temp;
}
Если тип T - это вполне определенный тип, например int, string или Person, то никаких проблем не существует, все совершенно прозрачно. Но как быть, если возникает необходимость обмена данными разного типа? Неужели нужно писать копии этой процедуры для каждого типа? Проблема легко решается в языках, где нет контроля типов - там достаточно иметь единственный экземпляр такой процедуры, прекрасно работающий, но лишь до тех пор, пока передаются аргументы одного типа. Когда же процедуре будут переданы фактические аргументы разного типа, то немедленно возникнет ошибка периода выполнения, и это слишком дорогая плата за
В типизированных языках, не обладающих механизмом универсализации, выхода практически нет - приходится писать многочисленные копии Swap.
До недавнего времени Framework .Net и соответственно язык C# не поддерживали Замечу, что хотя меня прежде всего интересовала реализация |
Для достижения T как ее параметр, такой же, как и сами аргументы x1 и x2. Суть
Под
Объявить класс C# универсальным просто: для этого достаточно указать в объявлении класса, какие из используемых им типов являются параметрами. Список
class MyClass<T1, ... Tn> {...}
Как и всякие формальные параметры, Ti являются именами (идентификаторами). В теле класса эти имена могут задавать типы некоторых
В C# универсальными могут быть как классы, так и все их частные случаи - интерфейсы, структуры, делегаты, события.
Специальным частным случаем swap:
class Change
{
static public void Swap<T>(ref T x1, ref T x2)
{
T temp;
temp = x1; x1 = x2; x2 = temp;
}
}
Как видите, сам класс в данном случае не имеет родовых параметров, но зато универсальным является статический метод класса swap, имеющий родовой параметр типа T. Этому типу принадлежат аргументы метода и локальная переменная temp. Всякий раз при вызове метода ему, наряду с фактическими аргументами, будет передаваться и T в описании метода. О некоторых деталях технологии подстановки и выполнения метода поговорим в конце лекции, сейчас же лишь отмечу, что реализация вызова универсального метода в C# не приводит к существенным накладным расходам.
Рассмотрим тестирующую процедуру из традиционного для наших примеров класса Testing, в которой интенсивно используется вызов метода swap для различных типов переменных:
public void TestSwap()
{
int x1 = 5, x2 = 7;
Console.WriteLine("до обмена: x1={0}, x2={1}",x1, x2);
Change.Swap<int>(ref x1, ref x2);
Console.WriteLine("после обмена: x1={0}, x2={1}", x1, x2);
string s1 = "Савл", s2 = "Павел";
Console.WriteLine("до обмена: s1={0}, s2={1}", s1, s2);
Change.Swap<string>(ref s1, ref s2);
Console.WriteLine("после обмена: s1={0}, s2={1}", s1, s2);
Person pers1 = new Person("Савлов", 25, 1500);
Person pers2 = new Person("Павлов", 35, 2100);
Console.WriteLine("до обмена: ");
pers1.PrintPerson(); pers2.PrintPerson();
Change.Swap<Person>(ref pers1, ref pers2);
Console.WriteLine("после обмена:");
pers1.PrintPerson(); pers2.PrintPerson();
}
Обратите внимание на строки, осуществляющие вызов метода:
Change.Swap<int>(ref x1, ref x2); Change.Swap<string>(ref s1, ref s2); Change.Swap<Person>(ref pers1, ref pers2);
В момент вызова метода передаются фактические аргументы и int и string и тип Person, определенный пользователем. Общая ситуация такова: если в классе объявлен универсальный метод со списком параметров M<T1, ...Tn> (...), то метод вызывается следующим образом: M<TYPE1, ... TYPEn>(...), где TYPEi - это конкретные типы.
Еще раз напомню, что все эти примеры построены в Whidbey, и вот как выглядят внешний вид среды разработки и окно с результатои работы этой процедуры.
(рис 22.1) Результаты работы универсальной процедуры swapВ этом примере использовался класс Person, и поскольку он появится и в следующих примерах, то приведу его текст:
class Person
{
public Person(string name, int age, double salary)
{
this.name = name; this.age = age; this.salary = salary;
}
public string name;
public int age;
public double salary;
public void PrintPerson()
{
Console.WriteLine("name= {0}, age = {1}, salary ={2}",
name, age, salary);
}
}
Эти механизмы взаимно дополняют друг друга. T заменяется TFact, то там, где разрешено появляться объектам типа TFact, разрешены и объекты, принадлежащие классам-потомкам TFact.
Эти механизмы в совокупности обеспечивают бесшовный процесс разработки программных систем, начиная с этапов спецификации и проектирования системы и заканчивая этапами реализации и сопровождения. На этапе задания спецификаций появляются абстрактные,

(рис 22.2.2) 1: Этап проектирования: абстрактный класс с абстрактными типами(рис 22.2.1) 2: Наследование: уточняется представление данных; задается или уточняется реализация методов родителя
(рис 22.2.3) 3: Родовое порождение: уточняются типы данных; порождается класс путем подстановки конкретных типовНа этапе спецификации, как правило, создается абстрактный,
Для наполнения этой схемы реальным содержанием давайте рассмотрим некоторый пример с прохождением всех трех этапов.
Возьмем классическую задачу определения
/// <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 t);
/// <summary>
/// require: true;
/// </summary>
/// <returns>true если стек пуст, иначе false </returns>
abstract public bool empty();
}// class GenStack
В приведенном примере программного текста чуть-чуть. Это объявление абстрактного
abstract public class GenStack<T>
и четыре строки с объявлением сигнатуры его методов. Основной текст задает описание спецификации класса и его методов. Заметьте, здесь спецификации заданы достаточно формально с использованием аксиом, характеризующих смысл операций, которые выполняются над
Не хочется вдаваться в математические подробности, отмечу лишь, что, если задать последовательность операций над
Наш класс является универсальным -
Наш класс является абстрактным - не задана ни реализация методов, ни то, как
Перейдем теперь ко второму этапу и построим потомков класса, каждый из которых задает некоторое представление
/// <summary>
/// Стек, построенный на односвязных элементах списка GenLinkable<T>
/// </summary>
public class OneLinkStack<T> : GenStack<T>
{
public OneLinkStack()
{
last = null;
}
GenLinkable<T> last; //ссылка на стек (вершину стека)
public override T item()
{
return (last.Item);
}//item
public override bool empty()
{
return (last == null);
}//empty
public override void put(T elem)
{
GenLinkable<T> newitem = new GenLinkable<T>();
newitem.Item = elem; newitem.Next = last;
last = newitem;
}//put
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)
По ходу дела нам понадобился класс, задающий представление элементов
public class GenLinkable<T>
{
public T Item;
public GenLinkable<T> Next;
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, но каждый из них работает с данными своего типа и по-разному реализует методы родителя. На рис. 22.3 показаны результаты работы этой процедуры.
(рис 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# допускаются три вида ограничений, накладываемых на родовые параметры.
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 ; так же, как и методы интерфейса IEnumerable<T1>, он может создавать объекты типа T1, используя T2, должен быть значимым, и объекты этого типа разрешается сравнивать между собой.
Ключевые идеи ограниченной int в нем не должно быть. По этим причинам класс должен быть универсальным, имея в качестве параметра тип T, задающий тип хранимых данных. Мы потребуем также, чтобы данные хранились с их ключами. И поскольку не хочется заранее накладывать ограничения на тип ключей - они могут быть строковыми или числовыми, - то тип хранимых ключей будет еще одним параметром нашего класса. Поскольку мы хотим определить над списком операцию поиска по ключу, то нам придется выполнять проверку ключей на равенство, поэтому универсальность типа ключей должна быть ограниченной. Проще всего сделать этот тип наследником стандартного интерфейса IComparable.
Чтобы не затемнять ситуацию сложностью списка, рассмотрим достаточно простой Node, два
class Node<K, T> where K:IComparable<K>
{
public Node()
{
next = null; key = default(K); item = default( T);
}
public K key;
public T item;
public Node<K, T> next;
}
Класс 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; cursor = 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. Все работает прекрасно. Вот результаты вычислений по этой процедуре:
(рис 22.5) Поиск в списке с ограниченной универсальностью
Представьте себе, что мы хотим иметь специализированный вариант нашего списка, элементы которого допускали бы операцию сложения и одно из
Как уже говорилось, наличие ограничения операции, где можно было бы указать, что над элементами определена операция +, решало бы проблему. Но такого типа ограничений нет. Хуже того, нет и интерфейса 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());
Как видите, конструктору объекта передается калькулятор, согласованный с типами данных, которые хранятся в списке. Результаты вычислений, полученных при работе с этими списками, приведены на рис. 22.6.
(рис 22.6) Списки с суммированием
До сих пор рассматривалась ситуация using, назначение которого и состоит в выполнении подобных подстановок.
Давайте вернемся к OneLinkStack<T>, введенному в начале этой лекции, и породим на его основе вполне конкретный класс IntStack, заменив формальный параметр T фактическим - int. Для этого достаточно задать следующее предложение using:
using IntStack = Generic.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);
}
Все работает заданным образом, можете поверить.
Так же, как и обычный класс, структура может иметь родовые параметры. Синтаксис объявления, ограниченная
public struct Point<T>
{
T x, y;//координаты точки, тип которых задан параметром
// другие свойства и методы структуры
}
Интерфейсы чаще всего следует делать IComparable<T> и других. Введение object, выполнения операций boxing и .
Делегаты также могут иметь родовые параметры. Чаще встречается ситуация, когда делегат объявляется в Delegate, в котором объявляется функциональный тип - delegate:
class Delegate<T>
{
public delegate T Del(T a, T b);
}
Как видите, тип аргументов и возвращаемого значения в сигнатуре функционального типа определяется классом Delegate.
Добавим в класс FunAr, одним из аргументов которой будет функция типа Del, заданного делегатом. Эта функция будет применяться к элементам массива, передаваемого также функции FunAr. Приведу описание:
public T FunAr(T[] arr, T a0, Del 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; }
Хотя все функции имеют разные типы, все они соответствуют определению класса Del - имеют два аргумента одного типа и возвращают результат того же типа. Посмотрим, как они применяются в тестирующем методе класса 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 };
Delegate<int> d1 = new Delegate<int>();
Delegate<int>.Del del1;
del1= this.max2;
int max = d1.FunAr(ar1, ar1[0], del1);
Console.WriteLine("max= {0}", max);
Delegate<double> d2 = new Delegate<double>();
Delegate<double>.Del del2;
del2 = this.min2;
double min = d2.FunAr(ar2, ar2[0], del2);
Console.WriteLine("min= {0}", min);
Delegate<string> d3 = new Delegate<string>();
Delegate<string>.Del del3;
del3 = this.sum2;
string sum = d3.FunAr(ar3, "", del3);
Console.WriteLine("concat= {0}", sum);
Delegate<float> d4 = new Delegate<float>();
Delegate<float>.Del del4;
del4 = this.prod2;
float prod = d4.FunAr(ar4, 1f, del4);
Console.WriteLine("prod= {0}", prod);
}
Обратите внимание на объявление
Delegate<int>.Del del1;
В момент объявления задается
del1= this.max2;
При выполнении этого присваивания производятся довольно сложные действия - проверяется соответствие сигнатуры функции в правой части и
Покажем, что и сам функциональный тип-делегат можно объявлять с родовыми параметрами. Вот пример такого объявления:
public delegate T FunTwoArg<T>(T a, T b);
Добавим в наш тестовый пример код, демонстрирующий работу с этим делегатом:
FunTwoArg<int> mydel;
mydel = max2;
max = mydel(17, 21);
Console.WriteLine("max= {0}", max);
Вот как выглядят результаты работы тестового примера:
(рис 22.7) Результаты работы с универсальными делегатамиEventHandler, применяемый для всех событий, не имеющих собственных аргументов, теперь дополнен универсальным аналогом, определенным следующим образом:
public void delegate EventHandler<T> (object sender, T args) where T:EventArgs
Этот делегат может применяться и для событий с собственными аргументами, поскольку вместо параметра T может быть подставлен конкретный тип - потомок класса EventArgs, дополненный нужными аргументами.
Решение этих задач потребовало введения
При этом дублирования кода не происходит и на уровне JIT-компиляторов, которые, однажды сгенерировав код для конкретного типа, сохраняют ссылку на этот участок кода и передают ее, когда такой код понадобится вторично. Это справедливо как для ссылочных, так и значимых типов.
Естественно, что
Так, например, в класс System.Array добавлен ряд универсальных статических методов. Вот один из них:
public static int BinarySearch<T>(T[] array, T value);
В таблице 22.1 показаны некоторые System.Collections.Generic и их аналоги из пространства System.Collections.
| Универсальный класс | Обычный класс | | Обычный интерфейс |
|---|---|---|---|
| Comparer<T> | Comparer | ICollection<T> | ICollection |
| Dictionary<K,T> | HashTable | IComparable<T> | IComparable |
| LinkedList<T> | ---- | IDictionary<K,T> | IDictionary |
| List<T> | ArrayList | IEnumerable<T> | IEnumerable |
| Queue<T> | Queue | IEnumerator<T> | IEnumerator |
| SortedDictionary<K,T> | SortedList | IList<T> | IList |
| Stack<T> | Stack |
Сериализация и универсализация также согласуются друг с другом, так что можно иметь
Необходимость в универсализации возникает с первых шагов программирования. Одна из первых процедур, появляющихся при обучении программированию - это процедура
public void Swap(ref T x1, ref T x2)
{
T temp;
temp = x1; x1 = x2; x2 = temp;
}
Если тип T - это вполне определенный тип, например int, string или Person, то никаких проблем не существует, все совершенно прозрачно. Но как быть, если возникает необходимость обмена данными разного типа? Неужели нужно писать копии этой процедуры для каждого типа? Проблема легко решается в языках, где нет контроля типов - там достаточно иметь единственный экземпляр такой процедуры, прекрасно работающий, но лишь до тех пор, пока передаются аргументы одного типа. Когда же процедуре будут переданы фактические аргументы разного типа, то немедленно возникнет ошибка периода выполнения, и это слишком дорогая плата за
В типизированных языках, не обладающих механизмом универсализации, выхода практически нет - приходится писать многочисленные копии Swap.
До недавнего времени Framework .Net и соответственно язык C# не поддерживали Замечу, что хотя меня прежде всего интересовала реализация |
Для достижения T как ее параметр, такой же, как и сами аргументы x1 и x2. Суть
Под
Объявить класс C# универсальным просто: для этого достаточно указать в объявлении класса, какие из используемых им типов являются параметрами. Список
class MyClass<T1, ... Tn> {...}
Как и всякие формальные параметры, Ti являются именами (идентификаторами). В теле класса эти имена могут задавать типы некоторых
В C# универсальными могут быть как классы, так и все их частные случаи - интерфейсы, структуры, делегаты, события.
Специальным частным случаем swap:
class Change
{
static public void Swap<T>(ref T x1, ref T x2)
{
T temp;
temp = x1; x1 = x2; x2 = temp;
}
}
Как видите, сам класс в данном случае не имеет родовых параметров, но зато универсальным является статический метод класса swap, имеющий родовой параметр типа T. Этому типу принадлежат аргументы метода и локальная переменная temp. Всякий раз при вызове метода ему, наряду с фактическими аргументами, будет передаваться и T в описании метода. О некоторых деталях технологии подстановки и выполнения метода поговорим в конце лекции, сейчас же лишь отмечу, что реализация вызова универсального метода в C# не приводит к существенным накладным расходам.
Рассмотрим тестирующую процедуру из традиционного для наших примеров класса Testing, в которой интенсивно используется вызов метода swap для различных типов переменных:
public void TestSwap()
{
int x1 = 5, x2 = 7;
Console.WriteLine("до обмена: x1={0}, x2={1}",x1, x2);
Change.Swap<int>(ref x1, ref x2);
Console.WriteLine("после обмена: x1={0}, x2={1}", x1, x2);
string s1 = "Савл", s2 = "Павел";
Console.WriteLine("до обмена: s1={0}, s2={1}", s1, s2);
Change.Swap<string>(ref s1, ref s2);
Console.WriteLine("после обмена: s1={0}, s2={1}", s1, s2);
Person pers1 = new Person("Савлов", 25, 1500);
Person pers2 = new Person("Павлов", 35, 2100);
Console.WriteLine("до обмена: ");
pers1.PrintPerson(); pers2.PrintPerson();
Change.Swap<Person>(ref pers1, ref pers2);
Console.WriteLine("после обмена:");
pers1.PrintPerson(); pers2.PrintPerson();
}
Обратите внимание на строки, осуществляющие вызов метода:
Change.Swap<int>(ref x1, ref x2); Change.Swap<string>(ref s1, ref s2); Change.Swap<Person>(ref pers1, ref pers2);
В момент вызова метода передаются фактические аргументы и int и string и тип Person, определенный пользователем. Общая ситуация такова: если в классе объявлен универсальный метод со списком параметров M<T1, ...Tn> (...), то метод вызывается следующим образом: M<TYPE1, ... TYPEn>(...), где TYPEi - это конкретные типы.
Еще раз напомню, что все эти примеры построены в Whidbey, и вот как выглядят внешний вид среды разработки и окно с результатои работы этой процедуры.
(рис 22.1) Результаты работы универсальной процедуры swapВ этом примере использовался класс Person, и поскольку он появится и в следующих примерах, то приведу его текст:
class Person
{
public Person(string name, int age, double salary)
{
this.name = name; this.age = age; this.salary = salary;
}
public string name;
public int age;
public double salary;
public void PrintPerson()
{
Console.WriteLine("name= {0}, age = {1}, salary ={2}",
name, age, salary);
}
}
Эти механизмы взаимно дополняют друг друга. T заменяется TFact, то там, где разрешено появляться объектам типа TFact, разрешены и объекты, принадлежащие классам-потомкам TFact.
Эти механизмы в совокупности обеспечивают бесшовный процесс разработки программных систем, начиная с этапов спецификации и проектирования системы и заканчивая этапами реализации и сопровождения. На этапе задания спецификаций появляются абстрактные,

(рис 22.2.2) 1: Этап проектирования: абстрактный класс с абстрактными типами(рис 22.2.1) 2: Наследование: уточняется представление данных; задается или уточняется реализация методов родителя
(рис 22.2.3) 3: Родовое порождение: уточняются типы данных; порождается класс путем подстановки конкретных типовНа этапе спецификации, как правило, создается абстрактный,
Для наполнения этой схемы реальным содержанием давайте рассмотрим некоторый пример с прохождением всех трех этапов.
Возьмем классическую задачу определения
/// <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 t);
/// <summary>
/// require: true;
/// </summary>
/// <returns>true если стек пуст, иначе false </returns>
abstract public bool empty();
}// class GenStack
В приведенном примере программного текста чуть-чуть. Это объявление абстрактного
abstract public class GenStack<T>
и четыре строки с объявлением сигнатуры его методов. Основной текст задает описание спецификации класса и его методов. Заметьте, здесь спецификации заданы достаточно формально с использованием аксиом, характеризующих смысл операций, которые выполняются над
Не хочется вдаваться в математические подробности, отмечу лишь, что, если задать последовательность операций над
Наш класс является универсальным -
Наш класс является абстрактным - не задана ни реализация методов, ни то, как
Перейдем теперь ко второму этапу и построим потомков класса, каждый из которых задает некоторое представление
/// <summary>
/// Стек, построенный на односвязных элементах списка GenLinkable<T>
/// </summary>
public class OneLinkStack<T> : GenStack<T>
{
public OneLinkStack()
{
last = null;
}
GenLinkable<T> last; //ссылка на стек (вершину стека)
public override T item()
{
return (last.Item);
}//item
public override bool empty()
{
return (last == null);
}//empty
public override void put(T elem)
{
GenLinkable<T> newitem = new GenLinkable<T>();
newitem.Item = elem; newitem.Next = last;
last = newitem;
}//put
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)
По ходу дела нам понадобился класс, задающий представление элементов
public class GenLinkable<T>
{
public T Item;
public GenLinkable<T> Next;
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, но каждый из них работает с данными своего типа и по-разному реализует методы родителя. На рис. 22.3 показаны результаты работы этой процедуры.
(рис 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# допускаются три вида ограничений, накладываемых на родовые параметры.
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 ; так же, как и методы интерфейса IEnumerable<T1>, он может создавать объекты типа T1, используя T2, должен быть значимым, и объекты этого типа разрешается сравнивать между собой.
Ключевые идеи ограниченной int в нем не должно быть. По этим причинам класс должен быть универсальным, имея в качестве параметра тип T, задающий тип хранимых данных. Мы потребуем также, чтобы данные хранились с их ключами. И поскольку не хочется заранее накладывать ограничения на тип ключей - они могут быть строковыми или числовыми, - то тип хранимых ключей будет еще одним параметром нашего класса. Поскольку мы хотим определить над списком операцию поиска по ключу, то нам придется выполнять проверку ключей на равенство, поэтому универсальность типа ключей должна быть ограниченной. Проще всего сделать этот тип наследником стандартного интерфейса IComparable.
Чтобы не затемнять ситуацию сложностью списка, рассмотрим достаточно простой Node, два
class Node<K, T> where K:IComparable<K>
{
public Node()
{
next = null; key = default(K); item = default( T);
}
public K key;
public T item;
public Node<K, T> next;
}
Класс 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; cursor = 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. Все работает прекрасно. Вот результаты вычислений по этой процедуре:
(рис 22.5) Поиск в списке с ограниченной универсальностью
Представьте себе, что мы хотим иметь специализированный вариант нашего списка, элементы которого допускали бы операцию сложения и одно из
Как уже говорилось, наличие ограничения операции, где можно было бы указать, что над элементами определена операция +, решало бы проблему. Но такого типа ограничений нет. Хуже того, нет и интерфейса 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());
Как видите, конструктору объекта передается калькулятор, согласованный с типами данных, которые хранятся в списке. Результаты вычислений, полученных при работе с этими списками, приведены на рис. 22.6.
(рис 22.6) Списки с суммированием
До сих пор рассматривалась ситуация using, назначение которого и состоит в выполнении подобных подстановок.
Давайте вернемся к OneLinkStack<T>, введенному в начале этой лекции, и породим на его основе вполне конкретный класс IntStack, заменив формальный параметр T фактическим - int. Для этого достаточно задать следующее предложение using:
using IntStack = Generic.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);
}
Все работает заданным образом, можете поверить.
Так же, как и обычный класс, структура может иметь родовые параметры. Синтаксис объявления, ограниченная
public struct Point<T>
{
T x, y;//координаты точки, тип которых задан параметром
// другие свойства и методы структуры
}
Интерфейсы чаще всего следует делать IComparable<T> и других. Введение object, выполнения операций boxing и .
Делегаты также могут иметь родовые параметры. Чаще встречается ситуация, когда делегат объявляется в Delegate, в котором объявляется функциональный тип - delegate:
class Delegate<T>
{
public delegate T Del(T a, T b);
}
Как видите, тип аргументов и возвращаемого значения в сигнатуре функционального типа определяется классом Delegate.
Добавим в класс FunAr, одним из аргументов которой будет функция типа Del, заданного делегатом. Эта функция будет применяться к элементам массива, передаваемого также функции FunAr. Приведу описание:
public T FunAr(T[] arr, T a0, Del 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; }
Хотя все функции имеют разные типы, все они соответствуют определению класса Del - имеют два аргумента одного типа и возвращают результат того же типа. Посмотрим, как они применяются в тестирующем методе класса 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 };
Delegate<int> d1 = new Delegate<int>();
Delegate<int>.Del del1;
del1= this.max2;
int max = d1.FunAr(ar1, ar1[0], del1);
Console.WriteLine("max= {0}", max);
Delegate<double> d2 = new Delegate<double>();
Delegate<double>.Del del2;
del2 = this.min2;
double min = d2.FunAr(ar2, ar2[0], del2);
Console.WriteLine("min= {0}", min);
Delegate<string> d3 = new Delegate<string>();
Delegate<string>.Del del3;
del3 = this.sum2;
string sum = d3.FunAr(ar3, "", del3);
Console.WriteLine("concat= {0}", sum);
Delegate<float> d4 = new Delegate<float>();
Delegate<float>.Del del4;
del4 = this.prod2;
float prod = d4.FunAr(ar4, 1f, del4);
Console.WriteLine("prod= {0}", prod);
}
Обратите внимание на объявление
Delegate<int>.Del del1;
В момент объявления задается
del1= this.max2;
При выполнении этого присваивания производятся довольно сложные действия - проверяется соответствие сигнатуры функции в правой части и
Покажем, что и сам функциональный тип-делегат можно объявлять с родовыми параметрами. Вот пример такого объявления:
public delegate T FunTwoArg<T>(T a, T b);
Добавим в наш тестовый пример код, демонстрирующий работу с этим делегатом:
FunTwoArg<int> mydel;
mydel = max2;
max = mydel(17, 21);
Console.WriteLine("max= {0}", max);
Вот как выглядят результаты работы тестового примера:
(рис 22.7) Результаты работы с универсальными делегатамиEventHandler, применяемый для всех событий, не имеющих собственных аргументов, теперь дополнен универсальным аналогом, определенным следующим образом:
public void delegate EventHandler<T> (object sender, T args) where T:EventArgs
Этот делегат может применяться и для событий с собственными аргументами, поскольку вместо параметра T может быть подставлен конкретный тип - потомок класса EventArgs, дополненный нужными аргументами.
Решение этих задач потребовало введения
При этом дублирования кода не происходит и на уровне JIT-компиляторов, которые, однажды сгенерировав код для конкретного типа, сохраняют ссылку на этот участок кода и передают ее, когда такой код понадобится вторично. Это справедливо как для ссылочных, так и значимых типов.
Естественно, что
Так, например, в класс System.Array добавлен ряд универсальных статических методов. Вот один из них:
public static int BinarySearch<T>(T[] array, T value);
В таблице 22.1 показаны некоторые System.Collections.Generic и их аналоги из пространства System.Collections.
| Универсальный класс | Обычный класс | | Обычный интерфейс |
|---|---|---|---|
| Comparer<T> | Comparer | ICollection<T> | ICollection |
| Dictionary<K,T> | HashTable | IComparable<T> | IComparable |
| LinkedList<T> | ---- | IDictionary<K,T> | IDictionary |
| List<T> | ArrayList | IEnumerable<T> | IEnumerable |
| Queue<T> | Queue | IEnumerator<T> | IEnumerator |
| SortedDictionary<K,T> | SortedList | IList<T> | IList |
| Stack<T> | Stack |
Сериализация и универсализация также согласуются друг с другом, так что можно иметь
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.