Так как экземпляры объектов являются динамическими, то для работы с ними предварительно требуется произвести необходимые действия по их созданию в куче. Эти действия выполняют специальные методы - конструкторы объектов. Любой конструктор перед выполнением записанных в его теле операторов вначале резервирует в куче место, необходимое для размещения объекта, и заполняет поля созданного объекта нулевыми значениями. После выполнения содержащихся в его теле операторов, конструктор, являясь функцией, возвращает адрес нового объекта. По умолчанию в каждом классе доступен стандартный конструктор Create, не содержащий внутренних операторов.
Созданный экземпляр объекта уничтожается другим методом — деструктором. Для разрушения объектов можно использовать деструктор Destroy, доступный любому объекту по умолчанию. Однако чаще всего применяют стандартный метод Free. Пользователь вправе написать свои конструктор или деструктор, указав их в описании класса.
Жизненный цикл объекта, состоящий из трех этапов (создание, использование и разрушение), может выглядеть примерно так:
Student:=TPerson.Create; // создание объекта
...
Writeln(Student.fname) // использование объекта
...
Student.Free; // разрушение объекта
Обратите внимание, имена вызываемых методов и полей объекта записываются через точку после имени объекта. Однако для метода Create сделано исключение: его применение к объекту напрямую невозможно, т.к. самого объекта еще не существует (т.н. ошибка доступа). Поэтому функция Create вызывается как метод класса (в данном случае TPerson) и возвращаемый ею адрес объекта присваивается ссылке Student.
Ниже представленный класс описывает точку на плоскости с координатами (x,y). Переменная данного класса содержит два поля (x и y), три метода (Ro, Move, GetSection) конструктор CreateLine и свойство Section (только для чтения).
type
TPoint = class
x,y : Real;
constructor CreateLine;
function Ro : real;
proceure Move(Dx,Dy : real);
function GetSection : byte;
property Section: byte read GetSection;
end;
constructor TPoint.CreateLine; // создает точку с координатами (1,1)
begin
x:=1;
y:=1
end;
function TPoint.Ro; // вычисляет расстояние до начала координат
begin
result:=Sqrt(Sqr(x)+Sqr(y));
end;
procedure TPoint.Move; // перемещает точку на вектор (Dx,Dy)
begin
x:=x+Dx;
y:=y+Dy;
end;
function TPoint.GetSection;
begin
if x*y=0
then Result:=0
else if x >0
then if y>0 then result:=1
else result:=4
else if y>0 then result:=2
else result:=3
end;
Жизненный цикл объекта данного класса может выглядеть примерно так:
var a : Tpoint;
begin
a:=TPoint.Create;
...
a.Move(5,-1.6);
...
a.Free;
end
Классическое правило объектно-ориентированного программирования утверждает, что для обеспечения надежности нежелателен прямой доступ к полям объекта: чтение и обновление их содержимого должно производиться посредством вызова соответствующих методов. Это правило называется инкапсуляцией.. В языке Object Pascal принцип инкапсуляции реализуется посредством свойств - виртуальных характеристик объектов, базирующихся на значениях конкретных полей.
Поля объекта хранят данные (т.е. занимают место в памяти), а свойства посредством вызова соответствующих методов позволяют определять и узнавать различные характеристики объектов, которые, в общем случае, могут и не храниться в памяти, а, например, вычислться по формулам.
Обычно свойство определяется полем соответствующего типа и двумя методами доступа – для чтения значения поля (функция) и изменения значения (процедура):
type
TMyClass = class
fValue : TSomeType;
function GetValue: TSomeType;
procedure SetValue(NewValue: TSomeType);
property pValue: TSomeType read GetValue write SetValue;
end;
В этом примере
fValue – поле для хранения значения свойства,
GetValue – метод для чтения значения свойства,
SetValue – метод для изменения значения свойства,
pValue – само свойство,
TSomeType – тип данного свойства.
В тексте программы использование свойства не отличается от использования обычного поля, но при компиляции вызов свойства для чтения или для записи заменяется вызовом соответствующих методов, например (var MyObject : TMyClass):
исходный текст
| интерпретируется как
|
MyObject.pValue:=AValue;
| MyObject.SetValue(AValue);
|
AVariable:= MyObject.pValue;
| AVariable:= MyObject.GetValue;
|
В рамках методов Get и Set прописываются дополнительные операции, которые необходимо выполнить для реализации чтения или изменения свойства. Например, при изменении ширины формы надо не просто занести в соответствующую ячейку новое значение, но и выполнить операции по перерисовке на мониторе как самой формы, так и окружающей ее области.
Если же дополнительные операции не требуются, то при описании свойства ссылку на метод можно заменить ссылкой на поле. Рассмотрим следующее описание:
type
TMyClass = class
fValue : TSomeType;
procedure SetValue(NewValue: TSomeType);
property pValue: TSomeType read fValue write SetValue;
function Correct(Value: TSomeType):boolean;
procedure DoSomeThing;
end;
...
procedure TmyClass.SetValue(NewValue: TSomeType);
begin
if Correct(NewValue) then fValue:=NewValue;
DoSomeThing;
end;
Здесь чтение свойства pValue означает просто чтение поля fValue. Зато при присвоении ему значения внутри SetValue будут вызваны сразу два метода.
Если свойство только читается или записывается, то в его описании присутствует только один, соответствующий, метод:
property pValue: TSomeType read GetValue;
В этом примере попытка изменить значение свойства pValue вызовет ошибку компиляции.
Часто количество свойств, основанных на группе полей может значительно превышать число самих полей. Например, описывая как объект точку на плоскости можно на основе двух полей – координат точки X и Y можно построить достаточно много свойств - характеристик точки: X и Y - декартовые координаты (для чтения и записи), r и j - полярные координаты (для чтения и записи), K- номер координатной четверти (от 1 до 4, только для чтения) и т.п.
(лекция 3)
Вторым основным принципом объектно-ориентированного программирования является наследование. Смысл его в том, что если вы желаете создать новый класс, лишь немногим отличающийся от уже описанного, то нет необходимости переписывания заново существующих полей и методов. Достаточно просто объявить, что новый класс порождается от существующего
type TNewClass = class (TOldClass),
т.е. является потомком илидочерним классом старого класса, называемого предком или родительским классом. При этом к новому классу автоматически (по умолчанию) будут добавлены все поля, методы и свойства старого класса. Поэтому, при описании потомка достаточно просто указать новые, дополнительные поля, методы и свойства.
Если в описании нового класса имя предка не указано (как в примерах выше) то новый класс порождается от класса TObject, ближними или далекими потомками которого являются все остальные классы. Кстати, методы Create, Free и Destroy наследуются именно от класса TObject.
Если имеет место совпадение имен новых и унаследованных полей и методов, то говорят, что они перекрываются. Рассмотрим пример перекрытия полей и статических методов:
type
T1 = class
a : integer;
procedure SetA(Value:integer);
end;
T2 = class (T1)
a : real;
procedure SetA(Value: real);
end;
...
procedure T1.SetA(Value:integer);
begin
a:=Value;
end;
procedure T2.SetA(Value:real);
begin
a:=0;
inherited SetA(Round(Value))
end;
В этом примере разные методы с именем SetA присваивают значения разным полям с именем a. В классе потомке одноименные поля и методы не наследуются, а перекрываются. Однако, если перекрытое поле предка недоступно вообще, то перекрытый метод доступен при указании зарезервированного слова inherited.