Тверской государственный университет
Опубликован: 22.11.2005 | Доступ: свободный | Студентов: 30274 / 1763 | Оценка: 4.31 / 3.69 | Длительность: 28:26:00
ISBN: 978-5-9556-0050-5
Лекция 22:

Универсальность. Классы с родовыми параметрами

Два основных механизма объектной технологии

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

Эти механизмы взаимно дополняют друг друга. Универсальность можно ограничить (об этом подробнее будет сказано ниже), указав, что тип, задаваемый родовым параметром, обязан быть наследником некоторого класса и/или ряда интерфейсов. С другой стороны, когда формальный тип T заменяется фактическим типом TFact, то там, где разрешено появляться объектам типа TFact, разрешены и объекты, принадлежащие классам-потомкам TFact.

Эти механизмы в совокупности обеспечивают бесшовный процесс разработки программных систем, начиная с этапов спецификации и проектирования системы и заканчивая этапами реализации и сопровождения. На этапе задания спецификаций появляются абстрактные, универсальные классы, которые в ходе разработки становятся вполне конкретными классами с конкретными типами данных. Механизмы наследования и универсализации позволяют существенно сократить объем кода, описывающего программную систему, поскольку потомки не повторяют наследуемый код своих родителей, а единый код универсального класса используется при каждой конкретизации типов данных. На рис. 22.2 показан схематически процесс разработки программной системы.

1: Этап проектирования: абстрактный класс с абстрактными типами

Рис. 22.2.1. 1: Этап проектирования: абстрактный класс с абстрактными типами
2: Наследование: уточняется представление данных; задается или уточняется реализация методов родителя

Рис. 22.2.2. 2: Наследование: уточняется представление данных; задается или уточняется реализация методов родителя
3: Родовое порождение: уточняются типы данных; порождается класс путем подстановки конкретных типов

Рис. 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>

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

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

Наш класс является универсальным - стек может хранить элементы любого типа, и конкретизация типа будет производиться в момент создания экземпляра стека.

Наш класс является абстрактным - не задана ни реализация методов, ни то, как стек будет представлен. Эти вопросы будут решать потомки класса.

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

/// <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; }
}
Александр Галабудник
Александр Галабудник

Не обнаружил проекты, которые используются в примерах в лекции, также не увидел список задач.

Александра Гусева
Александра Гусева