- •А. А. Волосевич
- •Содержание
- •1. Базовые понятия ооп
- •2. Создание и уничтожение объектов
- •3. Параметрself
- •4. Ограничение видимости элементов класса
- •5. Свойства
- •6. Отношения между классами
- •7. Перекрытие методов
- •8. Полиморфизм
- •9. Rtti и размещение объектов в памяти
- •10. Обработчики событий и указатели на методы
- •Литература
9. Rtti и размещение объектов в памяти
В Object Pascal после компиляции программы для любого класса сохраняется некая дополнительная информация, которая размещается в памяти непосредственно перед VMT. Эта информация называется информацией о типе периода времени выполнения(run-timetypeinformation, RTTI). Как было сказано выше, любой объект кроме данных полей содержит указатель на VMT (возможно на пустую таблицу, если у класса и его предков нет виртуальных методов). Следовательно, во время работы программы любой объект может получить доступ к RTTI своего класса. Схема размещения объектов и класса в памяти показана на рис. 1.
Рис. 1. Схема размещения объектов и RTTIв памяти
Известно, что в RTTI в числе прочих содержатся следующие данные:
1. Указатель на VMT класса-предка;
2. Указатель на строку с именем класса;
3. Размер экземпляра объекта в байтах.
Эти данные позволяют во время выполнения программы контролировать(type checking) иприводить(type casting) объектные типы.
Для контроля типов используется оператор is. Выражениеобъект is классвозвращаетtrue, еслиобъектпринадлежит указанному классу или потомкам этого класса:
if Man is TPerson then . . .
Для приведения типов используется оператор asв следующей форме:
(Man as TPerson).SetAge(10);
Допустима традиционная конструкция приведения типов в виде TPerson(Man).SetAge(10), однако операторasявляется более безопасным. В случае неудачи (то есть, когда объект не относится к указанному классу или его потомкам) он генерирует обрабатываемую исключительную ситуацию, а жёсткое приведение типов может привести к краху приложения.
10. Обработчики событий и указатели на методы
Рассмотрим класс TPersonи его методSetAgeдля установки возраста:
type TPerson = class
. . .
procedure SetAge(Age: Integer);
end;
procedure TPerson.SetAge(Age: Integer);
begin
if Age > 0 then fAge := Age
end;
Представим, что нужно дать пользователю контроль над ситуацией, когда значение параметра Ageне подходит для установки поля. Употребляя терминологию ООП, можно сказать: мы хотим дать пользователю контроль над определённымсобытием(неправильное значение параметра), происходящим при работе с объектом. Мы собираемся разрешить пользователю иметь обработчик данного события. Одно из решений, которое можно предложить в этой ситуации, заключается в следующем: поместим в классTPersonвиртуальный метод, вызываемый в результате события:
type TPerson = class
. . .
procedure SetAge(Age: Integer);
procedure DoSomething;virtual;
end;
procedure TPerson.SetAge(Age: Integer);
begin
if Age > 0 then fAge := Age
else DoSomething
end;
procedure TPerson.DoSomething;
begin
end;
Теперь для того, чтобы обработать событие, пользователю достаточно породить дочерний класс от TPersonи перекрыть методDoSomething:
type TMyPerson = class(TPerson)
procedure DoSomething;override;
end;
procedure TMyPerson.DoSomething;
begin
writeln('Something is wrong!')
end;
Такой подход для обработки событий имеет недостатки. Пользователи вынуждены «плодить» многочисленные производные классы, усложняя логику программы. Если бы в предыдущем примере планировалось создать десять объектов с разной обработкой события, мы вынуждены были бы породить от TPersonдесять дочерних классов.
Вместо виртуальных методов для обработки событий можно использовать процедурные типы. В этом случае в классе объявляется свойство соответствующего типа, а после создания объекта свойству присваивается написанная пользователем подпрограмма-обработчик:
type TProc = procedure;
type TPerson = class
. . .
fEvent: TProc;
procedure SetAge(Age: Integer);
property Event: TProc read fEvent write fEvent;
end;
procedure TPerson.SetAge(Age: Integer);
begin
if Age > 0 then fAge := Age
else fEvent // желательно дополнительно проверять fEvent на nil
end;
var Man, Woman: TPerson;
procedure ManEvent;
begin
writeln('Something is wrong with my age')
end;
procedure WomanEvent;
begin
writeln('I am 18 y.o.!')
end;
. . .
Man := TPerson.Create; // создание объектов
Woman := TPerson.Create;
Man.Event := ManEvent; // назначение обработчиков событий
Woman.Event := WomanEvent;
Man.SetAge(-10); // здесь эти обработчики сработают
Woman.SetAge(-10);
Подобный подход лучше, чем порождение многочисленных дочерних классов, однако он является не совсем объектно-ориентированным. По принципам «чистого» ООП программа – это набор взаимодействующих объектов различных классов. В нашем случае процедуры обработки события не являются методами класса, а «висят в воздухе». Попробуем объединить их в специальный класс, чтобы соблюсти принципы объектно-ориентированной разработки:
type TEventClass = class
procedure ManEvent;
procedure WomanEvent;
end;
procedure TEventClass.ManEvent;
begin
writeln('Something is wrong with my age')
end;
procedure TEventClass.WomanEvent;
begin
writeln('I am 18 y.o.!')
end;
Однако в таком варианте попытка назначить объектам обработчики вызывает синтаксическую ошибку:
var Man, Woman: TPerson;
EventObject: TEventClass;
. . .
EventObject := TEventClass.Create;
Man.Event := EventObject.ManEvent; // синтаксическая ошибка!
Хотя метод TEventClass.ManEventимеет сигнатуру типаTProc, он (как любой метод) получает неявный параметрself. Для манипуляций с методами необходимо использовать специальный процедурный тип, который называетсяуказателем на метод(methodpointer). Этот тип объявляется следующим образом:
type TProc = procedure of object;
Конструкцияof objectпосле сигнатуры подпрограммы говорит об объявлении указателя на метод. Переменная типа указатель на метод занимает 8 байтов и содержит адрес метода и ссылку на объект (т. е. значениеselfдля конкретного объекта). Если изменить объявление типа поля и свойства в классеTPersonна тип указателя на метод, то для обработки событий можно будет использовать методы другого класса (при условии совпадения сигнатур):
type TProc = procedure of object;
type TPerson = class
. . .
fEvent: TProc;
procedure SetAge(Age: Integer);
property Event: TProc read fEvent write fEvent;
end;
Подход, при котором методы обработки событий объектов сосредоточены в одном классе, называется делегированием.
Обработка событий и делегирование эффективно используется в Delphi. Любой компонент, помещаемый на форму при проектировании, представляет собой объект некоего класса. Компоненты обладают многочисленными событиями. Тип простейших событий с единственным параметром Senderописан следующим образом:
type TNotifiEvent = procedure(Sender: TObject) of object;
В качестве примера рассмотрим класс TEdit. Он обладает событиемOnClick. Для реализации работы с ним классTEditсодержит полеFOnClickи свойствоOnClick.
type TEdit = class(TCustomEdit)
. . .
FOnClick: TNotifiEvent;
property OnClick: TNotifiEvent read FOnClick
write FOnClick;
end;
Если в процессе разработки мы помещаем на форму компонент Edit1: TEditи пишем для него обработчик событияOnClick, то фактически мы пишем новый метод класса формыTForm1.Edit1Click. При создании формы в начале выполнения приложения метод формы связывается со свойством компонента (без нашего участия):
Edit1.OnClick := Form1.Edit1Click; // Form1 – объект классаTForm1
При щелчке на компоненте Edit1системой вызывается методTEdit.Click. В нем производится проверка наличия обработчика событияOnClickи вызов этого обработчика, если он существует.
if Assigned(OnClick) then OnClick(self);
Функция Assignedвозвращает значениеtrue, если её аргумент не равенnil. ПараметрSender, передаваемый обработчику события, позволяет различать объекты, которые вызвали событие. Это помогает использовать один обработчик для разных объектов.