Объекты
Чтобы от описания класса перейти к объекту, следует выполнить соответствующее объявление в секции var, например:
var MyClass: TMyClass;
При работе с обычными типами данных этого объявления было бы достаточно для получения экземпляра типа. Однако объекты в среде Delphi являются динамическими данными, т.е. распределяются в динамической памяти. Поэтому переменная MyClass – это просто ссылка на экземпляр (объект в памяти), которого физически еще не существует. Чтобы сконструировать объект (выделить память для экземпляра) класса TMyClass и связать с ним переменную MyClass, нужно в тексте программы поместить следующий оператор:
MyClass := TMyClass.Create;
где Create – имя конструктора. При создании объекта в памяти выделяется место только для его полей. Методы, как и обычные процедуры и функции, помещаются в область кода программы; они умеют работать с любыми экземплярами своего класса и не дублируются в памяти.
После создания объект можно использовать в программе: получать и устанавливать значения его полей, вызывать его методы. Доступ к полям и методам объекта происходит с помощью уточненных имен, например:
MyClass.Field := 137;
Writeln(MyClass.Func(348):10);
Кроме того, как и при работе с записями, допустимо использование оператора with.
Значение одной объектной переменной можно присвоить другой. При этом объект не копируется в памяти, а вторая переменная просто связывается с тем же объектом, что и первая:
Var r1, r2: tMyClass; // Переменные r1 и r2 не связаны с объектом
begin
R1 := TMyClass.Create; // Связывание переменной R1 с новым объектом
R2 := R1; // Связывание переменной R2 с тем же объектом, что и R1
end;
Объекты могут выступать в программе не только в качестве переменных, но также элементов массивов, полей записей, параметров процедур и функций. Кроме того, они могут служить полями других объектов. Во всех этих случаях программист фактически оперирует указателями на экземпляры объектов в динамической памяти. Следовательно, объекты изначально приспособлены для создания сложных динамических структур данных, таких как списки и деревья. Указатели на объекты для этого не нужны.
Свойства
В соответствии с принципом инкапсуляции, поля класса всегда должны быть защищены от несанкционированного доступа. Доступ к ним, как правило, должен осуществляться только через свойства, включающие методы чтения и записи полей. Поэтому поля обычно целесообразно объявлять в разделе private. Если надо обеспечить доступ к полям классов-наследников, то поля целесообразно размещать в разделах protected.
Традиционно идентификаторы полей, для доступа к которым создаются свойства, совпадают с именами соответствующих свойств, но с добавлением в качестве префикса символа «F». Свойство объявляется оператором вида:
property ИмяСвойства: Тип read ИмяПоляИлиИмяМетода write ИмяПоляИлиИмяМетода ДирективаЗапоминания;
Если в разделах read или write этого объявления записано имя поля, значит, предполагается прямое чтение или запись данных. Если в разделе read записано имя метода чтения, то чтение будет осуществляться только функцией с этим именем. Функция чтения - это функция без параметра, возвращающая значение того типа, который объявлен для свойства. Имя функции чтения принято начинать с префикса Get, после которого следует имя свойства. Если в разделе write записано имя метода записи, то запись будет осуществляться только процедурой с этим именем. Процедура записи - это процедура с одним параметром того типа, который объявлен для свойства. Имя процедуры записи принято начинать с префикса Set, после которого следует имя
свойства. Если раздел write отсутствует в объявлении свойства, значит, это свойство только для чтения и пользователь не может задавать его значение. Поля и методы чтения и записи, используемые в объявлении свойства, должны быть объявлены в этом классе до объявления свойства, или должны быть объявлены в родительском классе и доступны (т.е. не иметь спецификатора private). Директиву запоминания обычно имеет смысл задавать только для классов компонентов.
Рассмотрим пример задания свойств. Пусть требуется объявить класс с именем MyClass, наследующий непосредственно TObject и имеющий свойство целого типа с именем А. Тогда объявление этого класса может иметь вид:
type MyClass = class (TObject)
private
FA: Integer;
protected
procedure SetA(Value: Integer); // Процедура записи
public
property A: Integer read FA write SetA;
end;
Здесь вводится закрытое поле FА, объявляется защищенная функция SetA, используемая для записи значения этого поля, и вводится открытое свойство А, оперирующее этим полем. В объявлении, свойства после ключевого слова read записано просто имя поля. Это означает, что функция чтения отсутствует, и пользователь может читать непосредственно значение поля. После ключевого слова write следует ссылка на функцию записи SetA, с помощью которой будут записываться в поле A новые значения. В этой функции можно предусмотреть какие-то проверки допустимости вводимого значения А. Описание этой функции должно помещаться в раздел implementation того модуля, в котором объявлен класс. Если удалить из определения свойства А слово write с последующей ссылкой на функцию записи, то свойство станет свойством только для чтения, т. к, изменить его непосредственно будет невозможно.
Self
В теле метода можно обращаться ко всем полям класса и всем его методам, имеющим любые спецификаторы доступа. Обращение сводится просто к записи соответствующего идентификатора. Можно также использовать идентификатор Self. Этот идентификатор является ссылкой на тот объект, метод которого вызван. Так что если требуется получить доступ к некоторому полю объекта Fieldl, можно просто написать этот идентификатор, а можно сослаться на него через объект: Self.Fie1dl. Иногда это стоит делать, чтобы отличить идентификатор поля объекта от другого такого же или похожего идентификатора, имеющего другой смысл. А иногда ссылка Self просто необходима. Например, если в функции создается объект какого-то класса, наследующего классу TComponent, то в конструктор этого объекта надо передать владельца Owner этого объекта. Владельцем, очевидно, следует назначить тот объект, метод которого вызван, т.е. вызвать конструктор выражением ... Create(Self).
Методы
При описании нового класса можно добавлять новые методы и свойства, оставляя методы и свойства родителей, а можно родительские методы и свойства переопределить или перегрузить.
Имеется четыре вида методов: статические, виртуальные, динамические и абстрактные.
Статические методы
По умолчанию все методы статические. Если в классе-наследнике переопределить такой метод (ввести новый метод с тем же именем), то для объектов этого класса новый метод отменит родительский. Если обращаться к объекту этого класса, то вызываться будет новый метод. Но если обратиться к объекту как к объекту родительского класса, то вызываться будет метод родителя. Например:
type TFigure = class
procedure Draw;
end;
TRectangle = class(TFigure)
procedure Draw;
end;
var Figure: TFigure;
Rectangle: TRectangle;
begin
Figure := TFigure.Create;
Figure.Draw; // вызывается TFigure.Draw
Figure.Destroy;
Figure := TRectangle.Create;
Figure.Draw; // вызывается TFigure.Draw
TRectangle(Figure).Draw; // вызывается TRectangle.Draw
Figure.Destroy;
Rectangle := TRectangle.Create;
Rectangle.Draw; // вызывается TRectangle.Draw
Rectangle.Destroy;
end;
Виртуальные и динамические методы
Виртуальные и динамические методы не связаны с другими методами с тем же именем в классах-наследниках. Если в классах-наследниках эти методы перегружены, то при обращении к такому методу во время выполнения будет вызываться тот из методов с одинаковыми именами, который соответствует истинному классу объекта. Например, если имеется базовый класс графических объектов TFigure и ряд наследующих ему классов различных геометрических фигур, и если в каждом из этих классов определен свой виртуальный метод Draw, рисующий эту фигуру, то можно написать в программе:
var FigureArray: array[1..10] of TFigure;
begin
for i:=l to 10 do FigureArray[i].Draw;
end;
В этом коде в массив FigureArray могут помещаться объекты разных классов, наследующих TFigure. В цикле for обращение к объектам производится как к объектам базового для них типа TFigure. В этом случае для каждого объекта будет вызываться виртуальный метод Draw именно этого объекта. Такой подход, облегчающий работу с множеством родственных объектов, называется полиморфизмом.
При объявлении в классе виртуальных и динамических методов после точки с запятой, завершающей объявление метода, добавляются ключевые слова virtual или dynamic. Например:
type TFigure = class
procedure Draw; virtual;
end;
Чтобы перегрузить в классе-наследнике виртуальный метод, надо после его объявления поставить ключевое слово override. Например:
type TRectangle = class (TFigure)
procedure Draw; override;
end;
TEllipse = class (TFigure)
procedure Draw; override;
end;
Учитывая эти объявления, в следующем коде показан эффект вызова виртуального метода через переменную, фактический тип которой изменяется во время выполнения программы:
var Figure: TFigure;
begin
Figure := TRectangle.Create;
Figure.Draw; // вызывается TRectangle.Draw
Figure.Destroy;
Figure := TEllipse.Create;
Figure.Draw; // вызывается TEllipse.Draw
Figure.Destroy;
end;
Если в каком-то базовом классе метод был объявлен как виртуальный, то он остается виртуальным во всех классах-наследниках. Однако обычно для облегчения понимания кодов перегруженные методы принято повторно объявлять виртуальными, чтобы была ясна их суть для тех, кто будет строить наследников данного класса. Например:
type TEllipse = class (TFigure)
procedure Draw; override; virtual;
end;
Описанным способом могут быть перегружены только виртуальные и динамические методы. Различие между динамическими и виртуальными методами невелико и относится к внутреннему механизму реализации их вызовов. Виртуальные методы эффективнее с точки зрения временных затрат, а динамические с точки зрения затрат памяти. В целом виртуальные методы обеспечивают более эффективный механизм полиморфизма, а динамические более выгодны, если в базовом классе определено много перегружаемых методов и они одновременно используются многими объектами классов-наследников.
