Лекции (1129116), страница 26
Текст из файла (страница 26)
T1 y;
В этом случае, разрешено присваивание x=y, но неразрешено присваивание y=x. Тоже самое правило распространяется и на правила передачи параметров.
Что будет с динамическим типом объектов? Тут разные языки действуют по-разному. Их можно разделить на два класса – относительно статические языки и относительно динамические языки. В относительно статических языках (С++, Оберон) динамичность типа данных ограничена. В относительно динамических языках (Java, Delphi, Smalltalk) никаких ограничений на изменение типа нет.
Что происходит при таком присваивании? Есть два варианта: либо х меняет свой динамический тип, либо не меняет.
Чем же отличаются рассматриваемые нами языки? Объекты данных, под которые распределена память, не могут менять свой динамический тип. Т.е. динамически могут менять свой тип, в таких языках, как С++ и Оберон, только указатели и ссылки. Пусть типу Т соответствует указательный тип РТ, а типу Т1 соответствует тип РТ1. Тогда объекты типов РТ и РТ1 могут менять свой динамический тип. А объекты типов Т и Т1 свой динамический тип менять не могут. Это ограничение введено в язык из соображений эффективности, потому что иначе пришлось бы перераспределять память при каждом таком присваивании для смены динамического типа. Присваивание x=y в таких языках интерпретируется следующим образом.
О бъекту х присваивается только та часть объекта y, которая относится к типу Т.
Заметим, что обратное присваивание недопустимо, потому что тип Т1 богаче типа Т, к тому же, в этом случае, после присваивания, часть объекта y, относящаяся к типу Т, будет не определена. Т.е. динамический тип станет уже статического, а такого быть не может. То же самое относится и к передаче параметров. Если у нас есть функция F(T), то этой функции в качестве параметра можно передать объект типа Т1, потому что у него есть часть, относящаяся к типу Т. Если же есть функция G(T1), то понятно, что ей в качестве параметра передавать объект типа Т бессмысленно.
Рассмотрим, как такое присваивание интерпретируется в относительно динамических языках. Что общего между языком Delphi и Java? В частности то, что все объекты, объявленные как классы, являются на самом деле ссылками. Т.е. подход этих языков все равно близок к подходу С++ и Оберона, потому что там тоже можно менять динамический тип для ссылок и указателей. Но перед этим присваиванием программист обязан освободить память (в Java это сделает сборщик мусора), которую занимал объект х. Если же х имел пустое значение, то все в порядке. При этом перераспределения памяти не происходит, а в результате, х будет ссылаться на объект более богатого типа, чем тип объекта х. Т.е. х меняет свой динамический тип на тип Т1.
Теперь через объект х можно обращаться к полям типа Т1, но для этого нужно проверить динамический тип х. Для этого в Обероне была введена концепция динамической идентификации типа (для указателей и ссылок). Пусть есть процедура, параметром которой является ссылка на объект типа Т.
PROCEDURE P(VAR X:T);
Тогда в процедуре имеет смысл узнать динамический тип переменной Х, потому что он может отличаться от типа Т (т.е. тип объекта Х может быть производным от типа Т). Для этого есть две конструкции. Первая – это булевское выражение, которое имеет вид: t is T1, где t – это некоторое выражение, статический тип которого Т, а Т1 – это некоторый тип данных, производный (прямо или косвенно) от типа Т (иначе конструкция не имеет смысла и компилятор выдаст сообщение об ошибке). Эта проверка является истинной, если динамический тип данных выражения t либо совпадает с типом Т1, либо является производным от него. Т.е. если тип Т1 выведен из типа Т, а тип Т2 выведен из Т1, то проверка t is T1 будет истинной, если динамическим типом выражения t является Т1 или Т2. Структура любой переменной классового типа должна быть такова, чтобы иметь в себе информацию о динамическом типе.
Существует вторая конструкция – страж (guard) типа. Этот страж очень похож на преобразование типа и выглядит следующим образом: t(T1). Аналогично, t – это некоторое выражение, статический тип которого Т, а Т1 – это некоторый тип данных, производный от типа Т. Этот страж типа "говорит" следующее: "трактуй выражение t, как выражение типа T1. Т.е. если у типа Т есть свойство А, а у типа Т1 есть свойство В, то мы можем написать t.A, но не можем написать t.B, вместо этого нужно использовать страж типа: t(T1).B . Т.е. мы должны попросить компилятор проверить динамический тип t, и тогда можно обращаться к полям типа Т1.
IF t is T1 THEN //здесь выполняется проверка дин. типа
t(T1).B=… //здесь проверка дублируется и если все в порядке, //то происходит обращение к полю В, иначе – генерируется ошибка
В данном примере видно, что происходит лишняя проверка динамического типа, но к сожалению это необходимо для надежности. Есть модифицированная форма стража типа для сокращения записи и количества проверок:
WITH t:T1 DO //проверка выполняется только один раз
t.B=…
t.B=…
END;
Этот оператор внешне похож на оператор присоединения, который был в Паскале, но в Обероне это совсем другое.
В системе Delphi применяется такой же подход. Там тоже есть конструкция t is T1, при этом синтаксис и смысл этой конструкции совпадает с Обероном. Есть также преобразование t(T1), которое, в отличие от Оберона, не контролируется. Есть также два оператора присоединения – классический (with x do S), который позволяет обращаться к полям структуры непосредственно, и "обероновский" (with t as T1 do S), но здесь выполняется проверка типа, и если типы не совпадают, то генерируется исключительная ситуация, которую, по крайней мере, можно отловить.
Рассмотрим пример, который продемонстрирует необходимость применения вышеописанных средств. Подобного рода концепция динамизма часто оказывается очень полезной.
Допустим, мы программируем в системе Windows в системе Delphi, и нам нужно написать часть кода, которая позволяет динамически менять шрифт. Есть класс TFont, который инкапсулирует семантику шрифта в системе Windows. Есть стандартный диалог, который позволяет выбирать параметры шрифта и выдает TFont. У нас есть самые различные редакторы, инкапсулированные разными классами: TEdit (однострочный редактор), TMemo (многострочный редактор), TRichEdit. Семантика изменения шрифта для всех этих редакторов различна. TEdit и TMem – простейшие редакторы, и они одношрифтовые, а TRichEdit – это более сложный редактор, в нем допускаются сегменты текста в различных шрифтах. Поэтому, реакция на установление шрифта в разных редакторах разная. Если у нас есть объект Х типа TEdit, то мы можем написать так: X.Font:=Fnt (где Fnt – шрифт, полученный из стандартного диалога Windows).
У TRichEdit тоже есть свойство Font, но этот шрифт работает только как шрифт по умолчанию, т.е. таким шрифтом будут рисоваться все символы, которые вы будете набирать после этого. А хочется выделить некий сегмент текста и изменить у него шрифт. В Delphi есть свойство ActiveControl, которое говорит, в каком поле (т.е. элементе управления (Control), но для краткости – поле) сейчас находится клавиатурный фокус. Нам нужно как раз узнать динамический тип этого поля, чтобы знать, каким образом менять шрифт. При этом, нам нужно учесть все возможные типы таких полей, потому что в неучтенных типах полей наша программа работать не будет. В тоже время, мы можем использовать иерархию классов в Delphi, в которой есть класс TWinControl, который инкапсулирует в себе базовую функциональность любого поля в системе Windows. Из TWinControl в частности выводится класс TCustomEdit (все эти классы содержат различные свойства, чтобы программисту не приходилось задавать их самому), из которого выводятся классы TEdit, TRichEdit, TMem и др. Благодаря этим классам, процедуру изменения шрифта написать очень просто:
procedure ChFont(Fnt:TFont);
var Edit : TCustomEdit; //Мы рассчитываем, что все редакторы будут выведены из TCustomEdit
begin
if ActiveControl is TCustomEdit then //ActiveControl – свойство главной формы –
begin // понятие, которое есть в Delphi
Edit:=TCustomEdit(ActiveControl);
if Edit is TRichEdit then
with Edit as TRichEdit do
begin
//Изменяем шрифт
end
else Edit.Font=Fnt;
end;
end;
Этот код хорош тем, что он работает для любых полей, выведенных из TCustomEdit, потому что у всех этих полей будет атрибут Font. Причем этот код будет работать и для тех классов, которые уже созданы после создания этого кода, – этого на традиционных языках программирования сделать нельзя. Эта возможность основана на концепции динамической идентификации типа.
Р ассмотрим аспект распределения памяти. Каким образом при наследовании (единичном) распределяется память? Есть две модели: линейная модель и цепная. Линейной модели придерживаются такие языки, как Delphi, Java и С++. Пусть из класса Т выведен класс Т1, а из Т1 выведен класс Т2. Тогда в линейной модели памяти объекты этих классов будут размещены, как показано на рисунке. Линейная модель памяти удобна тем, что всегда достаточно иметь адрес начала объекта. Поэтому преобразование типа t(T1) к начальному адресу добавляет смещение нужного объекта. Линейная модель памяти наиболее эффективна.
Но существует цепная модель памяти. Эта модель памяти используется в языках Оберон и Смолток. В цепной модели памяти объекты представляются в виде списка. Т.е. у объекта типа Т2 будет указатель на объект типа Т1, а у того – на объект типа Т. Кроме того, должно быть поле, хранящее динамический тип данного объекта (оно будет присутствовать в старшем классе, т.е. в данном случае в Т). Преобразование t(T1) проверяет динамический тип объекта, и осуществляет поиск по линейному списку нужной составляющей объекта.
Модель распределение памяти не определяется какими-то правилами языка. Страуструп говорит, что для С++ наиболее оптимальной будет линейная модель, но никто не заставляет в реализации С++ придерживаться этой модели. Точно также, у Вирта нигде не написано о цепной реализации памяти, и программисту это знать на самом деле необязательно, но полезно.
Рассмотрим еще один пример на языке Оберон. Мы уже рассматривали пример, в котором был реализован контейнер (стек), содержащий объекты разных типов. Поэтому отпадает необходимость в статической параметризации. Организация гетерогенного стека была основана на типе Object:
Object = RECORD //Пустая запись, единственная роль которой
END; // служить корнем иерархии
PObject = POINTER TO Object; //указатель на объекты типа Object
//только указатели могут динамически менять тип в Обероне
….. //пишем стек, который работает с объектами типа Object
//в этом стеке можно сохранять объекты выведенные из Object
При работе с функцией стека POP мы должны смотреть, какой объект мы достали из стека, с помощью проверки типа (t is T). Работать с такими объектами можно с помощью стража типа (t(T1)). Но можно действовать несколько иначе. Допустим, что у нас есть некоторая совокупность графических объектов. Все графические объекты должны храниться в некотором контейнере, например, в линейном списке. Кроме того, все объекты обладают некоторыми общими свойствами, например, они умеют себя рисовать и перемещать. Мы уже писали костяк кода с помощью традиционного стиля программирования, и картина была неприглядной. Что же предлагает объектно-ориентированный стиль программирования?
TYPE Shape = POINTER TO NODE
NODE = RECORD //Звено линейного списка
NEXT:Shape;
END;
PROCEDURE Draw(P:Shape);
PROCEDURE Move(P:Shape; DX,DY:INTEGER);
TYPE Point = POINTER TO PointObj; //Точка
PointObj = RECORD(NODE)
X,Y:INTEGER;
END;
PROCEDURE DrawPoint(…);
PROCEDURE MovePoint (…);
TYPE Circle = POINTER TO CircleObj; //Окружность
CircleObj = RECORD(PointObj) // X,Y трактуется как центр окружности
R:INTEGER;
END;
PROCEDURE DrawCircle(…);
PROCEDURE MoveСircle(…);
Процедура рисования всех объектов будет выглядеть следующим образом:
PROCEDURE DrawAll(List:Shape)
BEGIN
WHILE List # NIL THEN
IF List is Point THEN DrawPoint(List(Point))
ELSEIF List is Circle THEN DrawCircle(List(Circle))
…
END;
List:=List.NEXT;
END;
END;
Вообще говоря, мы подобный код писали, рассматривая записи с вариантами. Каждый объект представлял собой запись с вариантами, и сущность этого объекта определялась дискриминантом. Структура программы, в сущности, была той же самой. Дал ли что-нибудь новый подход? Недостатком традиционного подхода было использование записи с вариантами, которая является ненадежным средством. Ничто не гарантирует соответствие дискриминанта и соответствующей области памяти.
Данный код очень похож на код в традиционном языке программирования, но он абсолютно надежен. Главным недостатком этого решения, является то, что при добавлении новых объектов, необходимо преобразовывать процедуру рисования. А подобных процедур в программе может быть очень много, и их все надо будет достроить. Т.е. код зависит от типов данных. Но это не недостаток объектно-ориентированного программирования – это недостаток соответствующей схемы. Мы можем написать более гибкий механизм, используя механизм т.н. обработчиков (Handler).
Лекция 22
В прошлый раз мы достаточно подробно разобрали типовую структуру ОО языков и вобщем-то ясно должно быть, что эта типовая структура является чистым расширением концепции уникальности типа, которую мы рассматривали в контексте обычных ЯП. У нас всегда есть возможность программировать на ОО языке в стиле, как мы программировали на хорешем процедурно-ориентированном языке типа Ada, Modula-2. Но, конечно, ОО языки предлагают существенно больше.