Новые концепции ООП в Object Pascal
32 урока по Delphi
Урок 26: Новые концепции ООП в Object Pascal
Данный материал принадлежит Сергею Орлику. ?1996 Все права защищены.
В языке Object Pascal, используемом в Delphi, произошел ряд давно ожидаемых программистами изменений, по сравнению с последней версией Borland Pascal. Перечислим, основные из них, позволившие назвать объектную модель Object Pascal новой объектной моделью:
- изменения в синтаксисе объявления и использования объектов
- введение функций классов
- введение методов классов
- изменения в организации определений и вызовов методов
-
введение раздела объявления интерфейса разработчика объектного типа - protected - введение раздела объявления design-time интерфеса объектного типа - published
- введение механизмов RTTI - информации о типах на этапе выполнения программ
- введение поддержки процедурных полей
- введение понятия "свойства" - property
В новой объектной модели программист работает только с динамическими экземплярами классов (то есть с теми, для которых выделяется память в heap-области), в отличие от старой модели, где можно было работать как с динамическими, так и со статическими экземплярами. По этой причине изменен синтаксис обращения к полям и методам объектов. Если раньше для работы с динамическими экземпляров объектов (инициализированными с использованием обращения к конструктору в сочетании с функцией New) программист должен был использовать обращение "по адресу" ( ^ ), то теперь такой доступ подразумевается автоматически. В качестве примера сравните два следующих фрагмента исходного текста:
{ Старая объектная модель }
type
PMyObject = ^TMyObject;
TMyObject = object (TObject)
MyField : PMyType;
constructor Init;
end;
...
var
MyObject : PMyObject;
begin
MyObject:=New(PMyObject,Init);
MyObject^.MyField:= ...
end;
{ Новая объектная модель }
type
TMyObject = class (TObject)
MyField : TMyType;
constructor Create;
end;
...
var
MyObject : TMyObject;
begin
MyObject:=TMyObject.Create;
MyObject.MyField:= ...
end;
Как Вы могли заметить, в Object Pascal расширен синтаксис использования “точечной нотации” для доступа к методам объектов. Кроме того, изменено соглашение и об именовании конструкторов и деструкторов. В старой объектной модели вызов New отвечал за распределение памяти, а обращение к конструктору инициализировало выделенную область памяти. В новой модели эти функции выполняет конструктор Create.
Приведем объявление базового для всех объектных типов класса TObject:
TObject = class
constructor Create;
destructor Destroy; virtual;
procedure Free;
class function NewInstance: TObject; virtual;
procedure FreeInstance; virtual;
class procedure InitInstance(Instance: Pointer): TObject;
function ClassType: TClass;
class function ClassName: string;
class function ClassParent: TClass;
class function ClassInfo: Pointer;
class function InstanceSize: Word;
class function InheritsFrom(AClass: TClass): Boolean;
procedure DefaultHandler(var Message); virtual;
procedure Dispatch(var Message);
class function MethodAddress(const Name: string):
Pointer;
class function MethodName(Address: Pointer): string;
function FieldAddress(const Name: string): Pointer;
end;
Компилятор Object Pascal является основой Delphi. Визуальные же средства Delphi построены на концепции Two-Way Tools, позволяющей синхронизировать процесс визуального проектирования форм приложения с генерацией исходного кода.
Такая архитектура возможна только при наличии механизма поддержки информации о типах - RTTI (RunTime Type Information). Основой такого механизма является внутренняя структура классов и, в частности, возможность доступа к ней за счет использования методов классов, описываемых конструкцией class function... Дадим определение понятия метода класса:
С одной стороны, Delphi, будучи визуальной средой разработки приложений, ориентирован на тех программистов, которые из готовых компонент "собирают" конкретные приложения для конечных пользователей. С другой стороны, являясь расширяемым объектно-ориентированным инструментом, этот продукт представляет интерес и для специалистов, занимающихся наращиванием функциональных возможностей уже существующих программных библиотек. Поэтому, выглядит абсолютно логичным появление в Object Pascal новых разделов в описании классов, соответственно, published и protected. Вместе с ранее введенными разделами (public и private) они предоставляют полный контроль над возможностями использования и "безболезненной" (в смысле предотвращения фатальных с точки зрения идеологии ошибок) модификации компонент Visual Component Library (VCL - библиотека классов Delphi). Чтобы была более ясна логика использования новых разделов, дадим, также, краткую характеристику и уже существующих:
- private - внутренние деталей реализации
- protected - интерфес разработчика
- public - run-time интерфейс
- published - design-time интерфейс
Раздел protected комбинирует функциональную нагрузку разделов private и public таким образом, что, если вы хотите скрыть внутренние механизмы вашего объекта от конечного пользователя, этот пользователь не сможет в run-time использовать ни одно из объявлений объекта из его protected области, но это не помешает разработчику новых компонент использовать эти механизмы в других модулях. То есть, protected-объявления доступны у любого из наследников вашего класса.
Раздел published
оказался необходимым при введении в Delphi возможности установки свойств и поведения компонент еще на этапе конструирования форм и самого приложения. Именно published-объявления доступны через Object Inspector, будь это ссылки на свойства или обработчики событий. Следует отметить тот факт, что, при порождении нового класса, возможен перенос объявлений из одного раздела в другой, с единственным ограничением - если вы производите скрытие объявления за счет его переноса в раздел private - в дальнейшем его "вытаскивание" у наследника в более доступный раздел в другом модуле будет уже невозможен. Такое ограничение, к счастью, не распространяется на динамические методы-обработчики сообщений Windows.
Учитывая, что наследование представляет собой один из краеугольных камней объектной идеологии, очевидной проблемой реализации объектной ориентированности языка является проблема диспетчеризации вызовов методов объектов.
Методы объектов Object Pascal могут иметь любой из трех типов: статический, виртуальный или динамический.
Так как статические и виртуальные методы не претерпели принципиальных изменений, по сравнению с Borland Pascal 7.0, остановимся на новом по реализации типе - динамическом (который, вообще говоря, присутствовал в неявном форме в библиотеке OWL).
Динамические (dynamic) методы, по возможностям наследования и перекрытия, аналогичны виртуальным, но в отличие от последних не имеют входов в таблицу VMT. Такой подход позволяет снизить расход памяти при большом количестве этих методов и самих классов.
В отличие от виртуальных методов и самой идеологии VMT, таблица динамических методов (DMT) содержит входы только для методов, объявленных или перекрытых для данного класса. На каждый динамический метод приходится только одна ссылка, представленная так называемым "индексом", по которому и происходит поиск метода для вызова (базовая информация по обработке динамических методов содержится в модуле x:\delphi\source\rtl\sys\dmth.asm). C точки зрения синтаксиса, перекрытие динамических и виртуальных методов производится одинаково - с использованием ключевого слова override. Исключение составляют обработчики Windows-сообщений wm_Xxx.
В Delphi существуют понятия, принципиально новые для уже существующих объектно-ориентированных реализаций Pascal. К числу этих понятий относятся свойства, функция класса и объектная ссылка.
В Object Pascal добавлена возможность определения полей процедурного типа. Очевидно, что в теле функций привязываемых к этим полям, разработчику необходим доступ к другим полям объекта, методам и т.п. Возможность такого доступа базируется на передаче в эти функции неявного, но доступного в их коде, параметра, автоматически принимающего значение поля объекта Self. Такие функции называются функциями классов. Для объявления функций классов необходимо использовать специальную конструкцию function ... of object.
Delphi позволяет вам создать специальный описатель объектного типа (именно типа, а не на экземпляра !), известный как object reference - объектная ссылка.
Объектные ссылки используются в следующих случаях:
- тип создаваемого объекта не известен на этапе компиляции
- необходим вызов метода класса, чей тип не известен на этапе компиляции
- в качестве правого операнда в операциях проверки и приведения типов с использованием is и as (о них мы будем говорить при обсуждении механизмов RTTI в главе 1.4)
type
TMyObject = class (TObject)
MyField:TMyObject;
constructor Create;
end;
TObjectRef = class of TObject;
...
var
ObjectRef:TObjectRef;
s:string;
begin
ObjectRef:=TMyObject; {присваиваем тип, а не экземпляр !}
s:=ObjectRef.ClassName; { строка s содержит ‘TMyObject’ }
end;
Таким образом в Delphi определена специальная ссылка TClass, совместимая по присваиванию с любым наследником TObject. Аналогично объявлены TPersistentClass и TComponentClass.
Методы в новой объектной модели используют те же соглашения о вызовах, что и обычные процедуры или функции, за некоторыми исключениями. "Ключом" внутренней организации вызовов методов объектов является тот факт, что для каждого метода, в дополнение к объявленным параметрам, передается неявный параметр Self, который описан для каждого класса или экземпляра объекта. Параметр Self всегда передается последним и представляет собой указатель. Для обычных методов, Self - указатель на экземпляр объекта, для методов классов - это указатель на Таблицу Виртуальных Методов (VMT). Например, для данного объявления:
type
TMyObject = class (TObject)
procedure One;
procedure Two; virtual;
class procedure Three; virtual;
end;
TMyClass = class of TObject;
...
var
MyObject:TMyObject;
MyClass:TMyClass;
вызов MyObject.One сгенерирует следующий код:
les DI,MyObject
push ES
push DI
call MyObject.One
При возврате управления, метод должен удалить из стека вначале параметр Self, а затем и остальные - явные параметры.
Методы всегда используют дальнюю (far) модель вызова, несмотря на то, каким образом установлена директива компиляции $F. Для вызова виртуального метода, компилятор генерирует код, загружающий из объекта указатель на VMT, и затем вызывает через точку входа в VMT ( ES:[DI] ), ассоциированный с этой точкой входа метод. Например, вызов MyObject.Two приведет к генерации следующего:
les DI,MyObject
push ES
push DI
les DI, ES:[DI]
call DWORD PTR ES:[DI]
Вызов MyObject.Three :
les DI,MyObject
les DI, ES:[DI]
push ES
push DI
call DWORD PTR ES:[DI+4]
; +4, т.к. это смещение в VMT для второго
; по счету виртуального метода
А для MyClass.Three будет сгенерированно:
les DI,MyClass
push ES
push DI
call DWORD PTR ES:[DI+4]
Очевидно, что приведенные примеры генерируемого кода соответствуют 16-разрядной версии Delphi, но по своей идеологии они остаются верной и для Delphi32.