Фундаментально про объектно-ориентированное программирование

Источник: DelphiSources
Федоренко Сергей

Введение

Язык программирования Object Pascal и его достойный преемник, среда программирования Delphi, построены на основе получившей широкое развитие на стыке 70 - 80-х годов 20 века теории объектно-ориентированного программирования. В то время идея описания программ в базисе логических сущностей и взаимодействия между ними не была такой уж бесспорной, а у некоторых оппонентов даже вызывала определённое недоумение. Преимущества ООП по сравнению с традиционными способами программирования:

  1. Эта концепция в наибольшей степени соответствует внутренней логике функционирования операционной системы Windows. Программа, состоящая из отдельных объектов, отлично приспособлена к реагированию на события, происходящие в операционной системе.
  2. Большая надёжность кода и возможность повторного использования отработанных объектов.

Объект и класс

Еже само название концепции "объектно-ориентированное программирование" указывает на то, что ключевой фигурой в ООП является объект. Объекты - это крупнейшее достижение в современной технологии программирования. Они позволили строить программу не из чудовищных по сложности процедур и функций, а из кирпичиков-объектов, заранее наделённых нужными свойствами. Самое приятное в объектах это то, что их внутренняя сложность скрыта от программиста, который просто пользуется готовым строительным материалом.

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

Объект не может возникнуть из воздуха, среда программирования каким-то образом должна быть проинформирована о его характеристиках. Поэтому предварительно программист описывает объект; такое описание называется классом. Класс - это чертёж будущего объекта, в котором учитываются не только его конструктивные элементы (поля), но и определяются способы управления этими элементами - методы класса.

Определение класса начинается с ключевого слова type глобального блока, за которым следуют имя класса, его поля и методы. Завершается описание директивой end. В объявление класса могут входить другие классы; таким образом можно создавать сложные составные объекты. По форме объявления классы похожи на обычные записи, но помимо полей данных могут содержать объявления пользовательских процедур и функций (методы). Пример:

type TPeople = class
Name: string;
Family: string;
procedure GetName;
procedure GetFamily;
end;

Класс содержит поля (Name, Family) и методы (GetName, GetFamily). Заголовки методов, всегда следующие за списком полей, играют роль упреждающих описаний. Программный код методов пишется отдельно от определения класса и будет приведён позже.
Чтобы от описания класса перейти к объекту, следует выполнить следующее объявление в секции var:

var People: TPeople;

При работе с обычными типами данных этого объявления было бы достаточно для получения экземпляра типа. Однако объекты среды Delphi являются динамическими данными, то есть распределяются в динамической памяти. Поэтому переменная People - это просто ссылка на экземпляр (объект в памяти), которого физически ещё не существует. Чтобы сконструировать объект (выделить память для экземпляра) класса TPeople и связать с ним переменную People, нужно в тексте программы поместить следующий оператор:

People := TPeople.Create; //Выделение памяти под объект

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

People.GetName;
People.GetFamily;

Кроме того, как и при работе с записями, допустимо использование оператора with, например:

with People do
GetFamily;
GetName;

Если объект становится ненужным, он должен быть удалён вызова специального метода Destroy, например:

People.Destroy; //Освобождение памяти, занимаемой объектом

Destroy - это так называемый деструктор объекта; он присутствует в классе наряду с конструктором и служит для удаления объекта из динамической памяти. После вызова деструктора переменная People становится несвязанной и не должна использоваться для доступа к полям и методам уже несуществующего объекта. Чтобы отличать в программе связанные объектные переменные от несвязанных, последнее следует инициализировать значением nil. Пример:

People := nil;
if People <> nil then People.Destroy;

Вызов деструктора для несуществующих объектов недопустим и при выполнении программы приведёт к ошибке. Чтобы избавить программистов от лишних ошибок, в объекты ввели предоставленный метод Free, который следует вызвать вместо деструктора. Метод Free сам вызывает деструктор Destroy, но только в том случае, если значение объектной переменной не равно nil. Поэтому последнюю строчку в приведённом выше примере можно переписать следующим образом:

People.Free;

После уничтожения объекта переменная People сохраняет своё значение, продолжая ссылаться на место в памяти, где объекта уже нет. Если эту переменную предполагается ещё использовать, то желательно присвоить ей значение nil, чтобы программа могла проверить, существует объект или нет. Таким образом, наиболее правильная последовательность действий при уничтожении объекта должна быть следующая:

People. Free;
People := nil;

С помощью стандартной процедуры FreeAndNil это можно сделать проще и элегантнее:

FreeAndNil(People); //Эквивалентно: People. Free; People := nil;

Значение одной объектной переменной можно присвоить другой. При этом объект не копируется в памяти, а вторая переменная просто связывается с тем же объектом, что и первая:

var People1, People2: TPeople;
begin
People1 := TPeople.Create;
People1 := People2;
People. Free;
end;

Объекты могут выступать в программе не только в качестве переменных, но также элементов массивов, полей записей, параметров процедур и функций. Кроме того, они могут служить полями других объектов. Во всех этих случаях программист фактически оперирует указателями на экземпляры объектов в динамической памяти. Следовательно, объекты изначально приспособлены для создания сложных динамических структур данных, таких как списки и деревья. Указатели на объекты для этого не нужны.

В некоторых случаях требуется, чтобы объекты разных классов содержали ссылки друг на друга. Возникает проблема: объявление первого класса будет содержать ссылку на ещё не определённый класс. Она решается с помощью упреждающего объявления:

type TPeople = class // Упреждающее объявление класса TPeople
THuman = class
Name: TPeople;
...
end;

type TPeople = class // Упреждающее объявление класса TPeople
People: array of THuman;
...
end;

Первое объявление класса TPeople называется упреждающим. Оно необходимо для того, чтобы компилятор нормально воспринял объявление поля Name в классе THuman.

Конструкторы и деструкторы

Особой разновидностью методов являются конструкторы и деструкторы. Создание объекта включает выделение памяти под экземпляр и инициализацию его полей, а разрушение - очистку полей и освобождение памяти. Действия по инициализации и очистке полей специфичны для каждого конкретного класса объектов. По этой причине язык Delphi позволяет переопределить стандартный конструктор Create и стандартный деструктор Destroy для выполнения любых полезных действий. Можно даже определить несколько конструкторов и деструкторов (имена им назначает сам программист), чтобы обеспечить различные процедуры создания и разрушения объектов.

Объявление конструкторов и деструкторов похоже на объявление обычных методов с той лишь разницей, что вместо зарезервированных слов function и procedure используются слова constructor и destructor. Пример:

type TPeople = class
Name: string;
Family: string;
procedure GetName;
  procedure GetFamily;
construcor Create;
destrucot Destroy;
end;

Возможная реализация:

procedure TPeople.Create;
begin
TPeople.Name := ' ';
TPeople.Family := ' ';
end;
procedure TPeople.Destroy;
begin
//Пока ничего не делаем
end;

Если объект содержит встроенные объекты или другие динамические данные, то конструктор - это как раз то место, где их нужно создавать. Конструктор применяется к классу или к объекту. Конструктор создаёт новый объект только в том случае, если перед его именем указано имя класса. Если указать имя уже существующего объекта, он поведёт себя по-другому: не создаст новый объект, а только выполнит код, содержащийся в теле конструктора. Если он применяется к классу,

People := TPeople.Create;

то выполняется следующая последовательность действий:

  1. В динамической памяти выделяется место для нового объекта.
  2. Выделенная память заполняется нулями. В результате все числовые поля и поля порядкового типа приобретают нулевые значения, строковые поля становятся пустыми, а поля, содержащие указатели и объекты получают значение nil.
  3. Затем выполняются заданные программистом действия конструктора. 
  4. Ссылка на созданный объект возвращается в качестве значения конструктора. Тип возвращаемого значения совпадает с типом класса, использованного при вызове (в нашем примере это тип TPeople).

Таким образом, хотя на первый взгляд синтаксис конструктора схож с вызовом процедуры (не определено возвращаемое значение), но на самом деле конструктор - это функция, возвращающая созданный и инициализированный объект. Если конструктор применяется к объекту,

People.Create;

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

Деструктор уничтожает объект к которому применяется:

People.Destroy;

В результате выполняются:

  1. Заданный программистом код завершения. 
  2. Освобождается занимаемая объектом динамическая память.
  3. В теле деструктора обычно должны уничтожаться встроенные объекты и динамические данные, как правило, созданные конструктором. Как и обычные методы, деструктор может иметь параметры, но эта возможность используется крайне редко.

Методы

Процедуры и функции, предназначенные для выполнения над объектами действий, называются методами. Предварительное объявление методов выполняется при описании класса в секции interface модуля, а их программный код записывается в секции implementation. Однако в отличии от обычных процедур и функций заголовки методов должны иметь уточнённые имена, то есть содержать наименование класса. Пример:

procedure TPeople.GetName;
begin
Writeln('Введите имя человека');
Readln(People.Name);
end;

Внутри методов обращения к полям и другим методам выполняются как к обычным переменным и подпрограммам без уточнения экземпляра объекта. Такое упрощение достигается путём использования в пределах метода псевдопеременной Self (стандартный идентификатор). Этот дополнительный скрытый параметр необходим в тех случаях, когда вы создаёте несколько объектов одного класса, так что каждый раз, когда вы применяете метод к одному из объектов, он должен оперировать именно со своими данными и не влиять на своих объектов-"братьев". Физически Self представляет собой дополнительный неявный параметр, передаваемый в метод при вызове. Этот параметр и указывает экземпляр объекта, к которому данный метод применяется. Практика показывает, что псевдопеременная Self редко используется в явном виде. Её необходимо применять только тогда, когда при написании метода может возникнуть какая-либо двусмысленность для компилятора.

Разграничение доступа к атрибутам объектов

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

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

Существует 4 спецификатора доступа: private, protected, public и published:

  1. Ключевое слово private определяет поля и методы класса, которые недоступны вне модуля (файла с исходным кодом), в котором определён класс. Секция позволяет скрыть те поля и методы, которые относятся к так называемым особенностям реализации.
  2. Ключевое слово public определяет поля и методы класса, к которым может обращаться любая часть кода программы (включая модуль, в котором определён класс). Всё, что помещается в секцию, служит для манипуляций с объектами и составляет программный интерфейс класса.
  3. Ключевое слово protected определяет частично доступные поля и методы. Доступ к ним имеют только методы данного класса и всех производных классов, остальным частям программы они не видны. Директива позволяет скрыть особенности реализации класса, но в отличие от private разрешает другим программистам порождать новые классы и обращаться к полям, методам и свойствам, которые составляют так называемый интерфейс разработчика. В эту секцию обычно помещаются виртуальные методы.
  4. Ключевое слово published устанавливает правила видимости те же, что и директива public. Особенность состоит в том, что для элементов, помещённых в секцию published, компилятор генерирует информацию о типах этих элементов. Эта информация доступна во время выполнения программы, что позволяет превращать объекты в компоненты визуальной среды разработки. Секцию published разрешено использовать только тогда, когда для самого класса или его предка включена директива компилятора $TYPEINFO.

Перечисленные секции могут чередоваться в объявлении класса в произвольном порядке, однако в пределах секции сначала следует описание полей, а потом методов и свойств. Если в определении класса нет ключевых слов private, public, protected, published, то для обычных классов всем полям, методам и свойствам приписывается атрибут видимости public, а для тех классов, которые порождены от классов библиотеки VCL, - атрибут видимости published. Внутри модуля никакие ограничения на доступ к атрибутам классов, реализованных в том же модуле, не действуют.

Свойства

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

type TPeople = class
FName: string;
procedure GetName;
construcor Create;
Destructor Destroy;
property Name: string read FName write GetName; // Свойство
end;

Ключевые слова read и write называются спецификаторами доступа. После слова read указывается поле или метод, к которому происходит обращение при чтении (получении) значения свойства, а после write - поле или метод, к которому происходит обращение при записи (установке) значения свойства. Чтобы имена свойств не совпадали с именами полей, последние принято писать с буквы F. Обращение к свойствам выглядит в программе как обращение к полям:

var People: TPeople;
Get: string;
...
People.Name := 'Сергей';
Get := People.Name;

Если один из спецификаторов доступа опущен, то значение свойства можно либо только читать (задан спецификатор read), либо только записывать (задан спецификатор write). Следующий пример объявлено свойство, значение которого можно только читать:

type TPeople = class
FName: array of string;
function GetName: integer;
property Name: integer read GetName;
end;

function TPeople.GetName: integer;
begin
Result := Length(FName);
end;

Здесь свойство Name показывает количество элементов в массиве FName. Поскольку оно определяется в результате чтения, пользователю объекта разрешено только количество элементов. В отличии от полей свойства не имеют адреса в памяти, поэтому к ним запрещено применять операцию @. Как следствие, их нельзя передавать в var- и out-параметрах процедур и функций. Технология объектно-ориентированного программирования в среде Delphi предписывает избегать прямого обращения к полям, создавая вместо этого соответствующие свойства. Это упорядочивает работу с объектами, изолирую их данные от непосредственной модификации. В будущем внутренняя структура класса, которая иногда является достаточно сложной, может быть изменена с целью повышения эффективности работы программы. При этом потребуется переработать только методы чтения и записи значения свойств; внешний интерфейс класса не изменится.

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

type TPeople = class
FName: boolean;
procedure SetName(const AName: boolean);
function GetName: integer;
property Name: boolean read FName write SetName;
property Count: integer read GetName;
end;

Использование методов для получения и установки свойств позволяет проверить корректность значения свойства, сделать дополнительные вычисления, установить значения зависимых полей и так далее.

procedure TPeople.SetName(const AName: boolean);
begin
if Name <> AName then
begin
if ANfme then // Если состояние изменяется то
...
else
...
FName := AName; // Сохранение состояния в поле
end;
end;

Один и тот же метод может использоваться для получения (установки) значений нескольких свойств одного типа. В этом случае каждому свойству назначается целочисленный индекс, который передаётся в метод чтения (записи) первым параметром. Пример:

type TPeople = class
...
property FirstName: string index 0 read GetItem;
property LastName: string index 1 read GetItem;
property Phone: string index 2 read GetItem ;
end;

var People: TPeople;
...
Writeln(People.FirstName); // Эквивалентно: Writeln(People.GetItem(0));
Writeln(People.LastName); // Эквивалентно: Writeln(People.GetItem(1));
Writeln(People.Phone); // Эквивалентно: Writeln(People.GetItem(2));
...

Кроме обычных свойств в объектах существуют свойства-массивы. Свойство-массив - это индексированное множество значений. Пример:

type TPeople = class
FName: array of string;
function GetName(index: integer): string;
property Name[index: integer]: string read GetName;
end;
function TPeople.GetName(index: integer): string;
begin
Result := FName[index];
end;

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

var People: TPeople;
i: integer;
...
for i:=0 to People.ItemCount - 1 do Writeln(People.Items[i]);
...

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

  1. Их индексы не ограничиваются диапазоном и могут иметь любой тип данных, а не только integer. Например, можно создать свойство-массив, в котором индексами будут строки. Обращение к такому свойству могло бы выглядеть примерно так:
    People.Items['Сергей'] := 'Александр';
  2. Операции над свойством-массивом в целом запрещены; разрешены операции только с его элементами.

Свойство-массив можно сделать основным свойством объектов данного класса. Для этого в описание свойства добавляется слово default:

type TPeople = class
...
property Name(index: integer): string read GetName; default;
...
end;

Такое объявление свойства Name позволяет рассматривать сам объект класса TPeople как массив и опускать имя свойства-массива при обращении к нему из программы, например:

var r: TPeople;
i: integer;
...
for I := 0 to R.ItemCount - 1 do Writeln(R[I]);

Следует помнить, что только свойства-массивы могут быть основными свойствами объектов; для обычных свойств это недопустимо.

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

property Name: string read FName write FName stored False; // отказ от запоминания

Три кита объектно-ориентированного программирования

Весь мир ООП держится на трех китах: инкапсуляции, наследовании и полиморфизме. Для начала о них надо иметь только самое общее представление.

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

Второй кит ООП - наследование. Этот простой принцип означает, что если вы хотите создать новый класс объектов, который расширяет возможности уже существующего класса, то нет необходимости в переписывании заново всех полей, методов и свойств. Вы объявляете, что новый класс является потомком (или дочерним классом) имеющегося класса объектов, называемого предком (или родительским классом), и добавляете к нему новые поля, методы и свойства. Процесс порождения новых классов на основе других классов называется наследованием. Новые классы объектов имеют как унаследованные признаки, так и, возможно, новые. Очень важно, что в отношениях наследования любой класс может иметь только одного непосредственного предка и сколь угодно много потомков. Поэтому все связанные отношением наследования классы образуют иерархию. Примером иерархии классов является библиотека VCL; с ее помощью в среде Delphi обеспечивается разработка GUI-приложений.

В языке Delphi существует предопределенный класс TObject, который служит неявным предком тех классов, для которых предок не указан. Это означает, что объявление

type TPeople = class
...
end;
эквивалентно следующему:
type TPeople = class(TObject)
...
end;

Класс TObject выступает корнем любой иерархии классов. Он содержит ряд методов, которые по наследству передаются всем остальным классам. Среди них конструктор Create, деструктор Destroy, метод Free и некоторые другие методы.

Поскольку класс TObject является предком для всех других классов (в том числе и для ваших собственных), то не лишним будет кратко ознакомиться с его методами:

type
 TObject = class
  constructor Create;
  procedure Free;
  class function InitInstance(Instance: Pointer): TObject;
  procedure CleanupInstance;
  function ClassType: TClass;
  class function ClassName: ShortString;
  class function ClassNameIs(const Name: string): Boolean;
  class function ClassParent: TClass;
  class function ClassInfo: Pointer;
  class function InstanceSize: Longint;
  class function InheritsFrom(AClass: TClass): Boolean;
  class function MethodAddress(const Name: ShortString): Pointer;
  class function MethodName(Address: Pointer): ShortString;
  function FieldAddress(const Name: ShortString): Pointer;
  function GetInterface(const IID: TGUID; out Obj): Boolean;
  class function GetInterfaceEntry(const IID: TGUID): PInterfaceEntry;
  class function GetInterfaceTable: PInterfaceTable;
  function SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult; virtual;
  procedure AfterConstruction; virtual;
  procedure BeforeDestruction; virtual;
  procedure Dispatch(var Message); virtual;
  procedure DefaultHandler(var Message); virtual;
  class function NewInstance: TObject; virtual;
  procedure FreeInstance; virtual;
  destructor Destroy; virtual;
 end;

Краткое описание методов в классе TObject:

  1. Create - стандартный конструктор.
  2. Free - уничтожает объект: вызывает стандартный деструктор Destroy, если значение псевдопеременной Self не равно nil.
  3. InitInstance(Instance: Pointer): TObject - при создании объекта инициализирует нулями выделенную память. На практике нет необходимости вызывать этот метод явно.
  4. CleanupInstance - освобождает память, занимаемую полями с типом string, variant, динамический массив и интерфейс. На практике нет необходимости вызывать этот метод явно.
  5. ClassType: TClass - возвращает описатель класса (метакласса).
  6. ClassName: ShortString - возвращает имя класса.
  7. ClassNameIs (const Name: string): Boolean - проверяет, является ли заданная строка именем класса.
  8. ClassParent: TClass - возвращает описатель базового класса.
  9. ClassInfo: Pointer - возвращает указатель на соответствующую классу таблицу RTTI. Таблица RTTI используется для проверки типов данных на этапе выполнения программы.
  10. InstanceSize: Longint - возвращает количество байт, необходимых для хранения в памяти одного объекта соответствующего класса. Значение, возвращаемое этим методом и значение, возвращаемое функцией SizeOf при передачи ей в качестве аргумента объектной переменной - это разные значения. Функция SizeOf всегда возвращает значение 4 (SizeOf(Pointer)), поскольку объектная переменная - это ни что иное, как ссылка на данные объекта в памяти. Значение InstanceSize - это размер этих данных, а не размер объектной переменной.
  11. InheritsFrom (AClass: TClass): Boolean - проверяет, является ли AClass базовым классом.
  12. MethodAdress (const Name: ShortString): Pointer - возвращает адрес published-метода, имя которого задаётся параметром Name.
  13. MethodName (Address: Pointer): ShortString - возвращает имя published-метода по заданному адресу.
  14. GetInterface (const IID: TGUID; out Obj): Boolean - возвращает ссылку на интерфейс через параметр Obj; идентификатор интерфейса задается параметром IID. (Интерфейсы рассмотрены в главе 6).
  15. GetInterfaceEntry (const IID: TGUID): PInterfaceEntry - возвращает информацию об интерфейсе, который реализуется классом. Идентификатор интерфейса задается параметром IID.
  16. GetInterfaceTable: PInterfaceTable - возвращает указатель на таблицу с информацией обо всех интерфейсах, реализуемых классом.
  17. AfterConstruction - автоматически вызывается после создания объекта. Метод не предназначен для явного вызова из программы. Используется для того, чтобы выполнить определенные действия уже после создания объекта (для этого его необходимо переопределить в производных классах.
  18. BeforeDestruction - автоматически вызывается перед уничтожением объекта. Метод не предназначен для явного вызова из программы. Используется для того, чтобы выполнить определенные действия непосредственно перед уничтожением объекта (для этого его необходимо переопределить в производных классах).
  19. Dispatch (var Message) - служит для вызова методов, объявленных с ключевым словом message.
  20. DefaultHandler (var Message) - вызывается методом Dispatch в том случае, если метод, соответствующий сообщению Message, не был найден.
  21. NewInstance: TObject - вызывается при создании объекта для выделения динамической памяти, чтобы разместить в ней данные объекта. Метод вызывается автоматически, поэтому нет необходимости вызывать его явно.
  22. FreeInstance - вызывается при уничтожении объекта для освобождения занятой объектом динамической памяти. Метод вызывается автоматически, поэтому нет необходимости вызывать его явно.
  23. Destroy - стандартный деструктор.
  24. FieldAdress (const Name: ShortString): Pointer - возвращает адрес published-поля, имя которого задается параметром Name.

В механизме наследования можно условно выделить три основных момента:

  1. Наследование полей.
  2. Наследование методов.
  3. Наследование свойств.

Наследование свойств и методов имеет свои особенности. Свойство базового класса можно перекрыть в производном классе, например чтобы добавить ему новый атрибут доступа или связать с другим полем или методом. Метод базового класса тоже можно перекрыть в производном классе, например чтобы изменить логику его работы.

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

Поскольку реальный экземпляр объекта может оказаться наследником класса, указанного при описании объектной переменной или параметра, бывает необходимо проверить, к какому классу принадлежит объект на самом деле. Чтобы программист мог выполнять такого рода проверки, каждый объект хранит информацию о своём классе. В языке Delphi существуют операторы is и as, с помощью которых выполняется проверка на тип и преобразование к типу. Пример проверки на принадлежность объекта Obj к классу TPeople или его наследнику:

var Obj: TObject;
...
if Obj is TPeople then
...

Для преобразования объекта к нужному типу используется оператор as:

with Obj as TPeople do // Равносильно: with TPeople(Obj) do
Active := False;

Вариант с оператором as лучше, поскольку безопасен. Он генерирует ошибку (точнее исключительную ситуацию) при выполнении программы, если реальный экземпляр объекта Obj не совместим с классом TPeople.

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

Виртуальные методы

Все методы, которые до сих пор рассматривались, являются статическими. Особенность такого метода заключается в его адресации. Она осуществляется ещё на стадии компиляции и компоновки проекта и будет неизменна (статична) до момента новой компиляции. Статическое связывание обладает существенным преимуществом над всеми остальными видами адресеции, поскольку обеспечивает самую высокую скорость вызова. Недостатком же фиксированной адресации является то, что статические методы не подлежат изменениям в классах-потомках. При обращении к статическому методу компилятор точно знает класс, к которому данный метод принадлежит. Объявление виртуального метода в базовом классе выполняется с помощью ключевого слова virtual, а его перекрытие в производных классах - с помощью ключевого слова override. Перекрытый метод должен иметь точно такой же формат (список параметров, а для функций ещё и тип возвращаемого значения), что и перекрываемый:

type TPeople = class
Name: string;
procedure GetName; virtual; // Виртуальный метод
end;
type TStudent = class(TPeople)
...
procedure GetName; override;
end;

Суть виртуальных методов в том, что они вызываются по фактическому типу экземпляра, а не по формальному типу, записанному в программе. Работа виртуальных методов основана на механизме позднего связывания. В отличие от раннего связывания, характерного для статических методов, позднее связывание основано на вычислении адреса вызываемого метода при выполнении программы. Адрес метода вычисляется по хранящемуся в каждом объекте описателю класса. Благодаря механизму наследования и виртуальных методов в среде Delphi реализуется такая концепция объектно-ориентированного программирования как полиморфизм. Полиморфизм существенно облегчает труд программистов, поскольку обеспечивает повторное использование кода уже написанных и отлаженных методов.

Работа виртуальных методов основана на косвенном вызове подпрограмм. При косвенном вызове команда вызова подпрограммы оперирует не адресом подпрограммы, а адресом места в памяти, где хранится адрес подпрограммы. Для каждого виртуального метода создаётся процедурная переменная, но её наличие и использование скрыто от программиста. Все процедурные переменные с адресами виртуальных методов пронумерованы и хранятся в таблице, называемой таблицей виртуальных методов. Такая таблица создаётся одна для каждого класса объектов, и все объекты этого класса хранят на неё ссылку.

Вызов виртуального метода происходит следующим образом:
   1. Через объектную переменную выполняется обращение к занятому объектом блоку памяти.
   2. Далее из этого блока извлекается адрес таблицы виртуальных методов (он записан в четырёх первых байтах).
   3. На основании порядкового номера виртуального метода извлекается адрес соответствующей подпрограммы.
   4. Вызывается код, находящийся по этому адресу.

При построении иерархии классов часто возникает ситуация, когда работа виртуального метода в базовом классе не известна и наполняется содержанием только в наследниках. Директива abstract записывается после слова virtual и исключает необходимость написания кода виртуального метода для данного класса. Такой метод называется абстрактным, то есть подразумевает логическое действие, а не конкретный способ его реализации. Абстрактные виртуальные методы часто используются при создании полу готовых классов. Свою реализацию такие методы получают в законченных наследниках.

Разновидностью виртуальных методов являются динамические методы. При их объявлении вместо ключевого слова virtual записывается ключевое слово dynamic. Динамические методы перечислены в специальном списке отдельно от таблицы виртуальных методов. В список динамических методов конкретного класса включены только адреса методов, описанных в данном классе. Поиск необходимого метода производится в обратном порядке дерева наследования. Если метод не найден в самом последнем дочернем классе, то поиск продолжается в его предке и так далее до TObject. В наследниках динамически методы перекрываются также, как и виртуальные - с помощью зарезервированного слова override. Если вы по какой-либо причине забудете указать директиву override, то унаследованный метод будет скрыт (но не отменён). Если вы решили спрятать предварительно объявленный метод, то поможет в этом директива reintroduce. Эта директива подавляет сообщения компилятора относительно уже существующего одноименного виртуального (динамического) метода в классе-предке. Задача команды inherited - принудительный вызов унаследованного от предка метода из метода, переопределяемого потомком (вызов исходного метода родительского класса). По смыслу динамические и виртуальные методы идентичны. Различие состоит только в механизме их вызова. Виртуальные методы вызываются максимально быстро, но платой за это является большой размер системных таблиц, с помощью которых определяются их адреса. Размер этих таблиц начинает сказываться с увеличением числа классов в иерархии. Динамические методы вызываются несколько дольше, но при этом таблицы с адресами методов имеют более компактный вид, что способствует экономии памяти.

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

type TWidgetControl = class(TControl)
...
procedure CMKeyDown (var Msg: TCMKeyDown); message CM_KeyDown;
...
end;

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


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=4382