лекции (2003) (Глазкова) (1160821), страница 12
Текст из файла (страница 12)
В языке С++ запись является частным случаем класса, и можно самому переопределить операцию сравнения.
Например,
class string {
...
bool operator < (const String& s);
...
}
Рассмотрим теперь как структуры (записи) реализованы в различных ЯП.
В языке С++ очень удачным оказалось то, что структура является частным случаем класса.
В языке Delphi есть отдельно понятие записи ( такое как в Turbo Pascal) и есть понятие класса, и это совершенно разные понятия прежде всего потому, что запись может иметь только переменные поля, а класс - это ссылочная структура.
Еще одно отличие : записи не могут наследоваться, а классы могут.
В языке С++ старое понятие записи является частным случаем нового понятия класса. Структура в языке С++ является классом с единственным отличием : по умолчанию все члены struct имеют публичный доступ, class - приватный.
struct s{ class s{
private: ….
... };
};
В языках C# и Java, которые изначально проектировались как объектно-ориентированные, понятие записи фактически отсутствует: его полностью заменило понятие класса.
В языке Oberon есть понятие записи, однако она является обычным классом. В языке C#, как и в языке Java, основное понятие - класс. Однако в языке C# есть и понятие структуры, синтаксически очень близкое к понятию класса, хотя между структурой и классом в C# есть существенное отличие.
В языках C# и Java существует единственный класс, который является прародителем всех остальных. Он называется Object в языке Java, object в языке C#, Тobject в языке Delphi.
Структура может быть неявно унаследована только от ТД Object. Саму структуру наследовать нельзя.
Самое главное отличие: структуры не имеют ссылочной семантики; класс всегда имеет ссылочную семантику.
Почему появилось понятие структуры в языке C#?
Из соображений эффективности.
В чем же неэффективность класса?
В описании языка C# приводится пример из языка Java.
Рассмотрим
class Point{
int x,y;
...
}
Рассмотрим процедуру, которая рисует ломаную. Вполне естественно задавать ее аргумент в виде массива точек - координат ломаной.
Point[ ] arg = new Point[1000];
Этот массив состоит из 1000 ссылок на структуру данных, которая имеет поля x и y.
рис.
При этом надо отвести память размером в 1000 указателей, и после этого проинициализировать каждый указатель. Расход памяти при этом 4 байта*3*1000.
Рассмотрим альтернативную реализацию.
В языке C# предлагается брать реализацию именно в виде структуры.
struct Point{
int x,y;
...
}
Point [ ] arg = new Point [1000];
По синтаксису все то же самое.
Но как выглядит соответствующий массив?
Поскольку структура не имеет ссылочной семантики, будет отведен массив из 1000 элементов, каждый из которых содержит х и y.
рис.
При этом расход памяти 4 байта*2*1000, т.е. на треть меньше.
Т.о., при использовании структуры получаем эффективность как по времени выполнения, так и по памяти. И именно из соображений эффективности в языке C# появилось понятие структуры, которое не имеет ссылочной семантики.
В языке Ада пошли по пути языка Delphi. На Delphi можно программировать в стиле Turbo Pascal ( не объектно-ориентированно).
Такой же подход и в языке Ада 95. В нем есть старое понятие записи, а для понятия объекта есть понятие тегированной записи (т.е. класс, с точки зрения объектно-ориентированной терминологии).
Записи в современных ЯП вытесняются более общим понятием класса.
Пункт 3.
Объединения (записи с вариантами в языке Pascal).
Интересно, что записи остались в некоторых современных ЯП (например, C#) из соображений эффективности, хотя понятно, что запись является частным случаем более общего понятия класса.
Записи с вариантами ни в одном из современных языков не остались, за исключением тех языков, в которых требовалась полная совместимость (Delphi, C++).
Для чего нужно понятие объединения ( или записи с вариантами)?
Это есть механизм объединения типов.
Зачем это нужно?
В традиционных ЯП концепция типа страдает одним большим недостатком: считается, что любой объект данных принадлежит единственному типу данных. Т.е. все множество объектов в программе разбивается на непересекающиеся классы эквивалентности, которые мы и называем ТД, и они объединяются между собой единым набором значений и операций. При этом смешивать между собой различные ТД, как правило, нельзя. (хотя большинство ЯП допускают неявные преобразования). Совершенно очевидно, что такое допущение является слишком сильным.
В связи с этим возникает так называемая янус-проблема (Янус - двуликий бог).
Янус-проблема состоит в том, что объекты реального мира никогда не являются принадлежностью определенного класса.
Рассмотрим, например, информационную систему, которая автоматизирует работу библиотеки. Понятие единицы хранения (книга), безусловно, является базисным в этой информационной системе. Какими же характеристиками должна обладать книга в такой информационной системе. Любая программная модель чем-то будет отличаться от объекта реального мира ( от чего-то мы должны абстрагироваться). От чего мы можем абстрагироваться в информационных системах библиотек? Например, от физических характеристик книги (размер, вес). Количество страниц - это важная характеристика. С другой стороны, как только мы начинаем планировать переезд библиотеки, то физические характеристики начинают играть значительную роль.
Когда элементами информационной системы выступают записи о людях, ситуация еще более сложная (в одном случае важен один набор параметров, в другом - другой).
В чем недостаток допущения, что любой объект принадлежит единственному типу данных?
Классический пример - системы интерфейса с пользователем.
Все эти системы, как правило, основаны на понятии события. Естественно, что каждому понятию должен соответствовать некий ТД.
Пусть мы пишем систему интерфейса с пользователем. С помощью какого ТД можно описать события (Event), какие параметры описывают событие?
Здесь мы как раз сталкиваемся с тем, что имеется несколько видов событий. Например,
KbdEvent, MouseEvent, CommonEvent (время совершения события - единственная общая характеристика у всех видов событий), коммуникационное событие и т.д.
Мы видим, что практически каждое устройство ввода-вывода добавляет свой класс событий. Единственное общее у этих классов событий - системное время наступления события.
В то же время, понятно, что элементы пользовательского интерфейса должны откликаться на события, т.е. у каждого класса должна быть своя процедура Handler (Event).
Хотелось бы иметь единый обработчик для всех событий (вместо того, чтобы на каждый тип события свой обработчик). Почему? Т.к. при добавлении одного нового устройства ввода-вывода появляется новый класс событий, и для каждого класса мы должны дописать по одной процедуре. В результате модифицировать придется всю систему.
Хотелось бы иметь одну процедуру-обработчик, чтобы она умела реагировать на всевозможные события.
Как же описать параметр такой процедуры?
Надо взять объединение типов. У нас есть совокупность совершенно различных типов. Их надо объединить в одно понятие - событие.
Для этого и существует понятие записи с вариантами (или объединения).
Объединения делятся на два типа:
-
размеченные
-
неразмеченные.
Размеченные объединения - это частный случай записи.
Структура размеченного объединения следующая : у него есть постоянная часть, есть специальное поле - дискриминант и есть вариантная часть.
постоянная часть | |||
дискриминант | |||
структура1 | структура2 | структура3 | вариантная часть |
В нашем примере постоянная часть - время совершения события, дискриминант - поле типа EventType; в зависимости от значений дискриминанта надо описать соответствующую структуру.
На языке Pascal объединения имеют вид:
record
пост_часть
case[имя] тип // это дискриминант
значение 1: структура 1 {…}
значение 2: структура 2{…}
…………
end
Каждая из этих частей может быть пустая ( если, например, отсутствует вариантная часть, то имеем классическое понятие записи).
Стандартный Pascal допускает как размеченные, так и неразмеченные объединения (если имя: отсутствует, то имеем неразмеченное объединение).
Рассмотрим объединения в языке С.
Это неразмеченные объединения вида:
union имя {
структура1 s1;
структура2 s2;
.…….
}
Т.е. все члены структуры размещаются с одного и того же адреса (это характерно именно для вариантов).
В языке С объединения только неразмеченные.
Чем удобны неразмеченные объединения? Почему в языке стандартный Pascal появилась возможность неразмеченного объединения?
Например, для побитовой обработки целого числа (учитывая, что стандартных побитовых операций в Pascal нет).
В этом случае неразмеченное объединение можно записать следующим образом
type T=record
case boolean of
true:(x: integer);
false:(a: packed array [1..48] of boolean);
end;
В результате у нас появилась возможность описать переменную var y:T; и после этого y.x=i; (т.е. рассматривать у как целое) или у.а [3] (т.е. работать с битами числа)
Как правило, неразмеченные объединения были нужны для доступа к внутренним частям примитивных типов данных.
Если речь идет о размеченном объединении, обработчик событий. как правило, имеет следующий вид:
procedure Handler (var Е: Event)
...
case E.дискриминант of
KbdEvent: операторы;
MouseEvent: ...
………..
end
Даже по синтаксису структура переключателя была приближена к вариантной записи.
Чем плох такой стиль программирования?
Ненадежность (никаких средств контроля записи с вариантами не содержат). Например, мы можем неправильно обрабатывать конкретные значения дискриминанта, или значение дискриминанта может быть испорчено.
Еще одна проблема - это проблема сопровождения.
Например, какие изменения нужно внести в систему, если добавляется новый класс устройства и связанные с ним события.
Во-первых, в описание типа Event добавляется еще один вариант (еще одно значение дискриминанта). Но самое неприятное, что для каждого класса мы должны проанализировать его обработчики и внести изменения. Т.е. мы должны иметь доступ ко всем исходным текстам и должны просмотреть практически весь исходный текст. В результате даже сравнительно небольшое изменение (такое как добавление нового устройства ввода-вывода) приводит к пересмотру всего текста.
Теперь обсудим некоторые аспекты, связанные с реализацией объединений в разных языках.
Первый вопрос - каким образом отводится память?
В языках Pascal и Modula2 память отводится по максимальному варианту.
Если же мы хотим отводить память только по тому варианту, который нам нужен, в языке Pascal для этого служит оператор new(P), где Р - указатель.
Оператор new возвращает указатель на тип данных Т и размещает в памяти объект.
В С несколько другой синтаксис NEW (P,t1,..,tn);
По этому синтаксису, Т должно быть вариантной записью, а ti - i-й дискриминант (дискриминанты могут быть вложенными, т.е. каждая структура тоже может быть записью с вариантами).
В этом случае менеджер динамической памяти отводит память в соответствии с тем вариантом, на который указывает соответствующий дискриминант, но сама запись при этом не инициализируется.
Обязательно надо инициализировать
P^.D1:=t1; P^.D2:=t2;...