Современные технологии программирования |
Конспект лекций |
Лекция 5. Классы Delphi
Особенности объектно-ориентированного программирования (ООП) - модели Object Pascal
Класс – тип данных, шаблон для создания объектов
Объект – структурный элемент приложения
Уровни представления программы: логический, языка программирования, физический
Описание классов
Объекты и их жизненный цикл
Объект на физическом уровне
Класс
Объект
Поле
Метод
Свойство
Self – ссылка на текущий объект в методе
RTTI класса
Класс Tobject
Конструктор
Деструктор
Указатель на объект
Класс - тип, объект (экземпляр класса), переменная типа (указатель на объект, ссылка на объект), метод, поле, свойство. Self. Класс Tobject – предок по умолчанию. Отличие метода от подпрограммы. Опережающее объявление класса. RTTI класса. Память под объект класса. Объекты и их жизненный цикл. Объект - динамическая переменная. Переменная типа класс – указатель. Конструктор (Create). Конструктор – метод класса. Деструктор (Destroy). Метод Free. Директива inherited. Создание и уничтожение компонентов формы. Создание и уничтожение форм. Примеры.
Особенности ООП - модели Object Pascal
Уровни представления программы
Программу можно рассматривать на трёх уровнях:
На логическом уровне в основе ООП лежит выделение в решаемой задаче объектов, и описание соответствующих им классов. Каждый объект (экземпляр класса) характеризуется набором признаков, к которым относят свойства, выраженные атрибутами и поведение, описываемое операциями, выполняемыми объектом.
На уровне языка программирования для реализации объектно-ориентированных проектов вводятся классы, объекты (экземпляры классов). Класс содержит описание признаков объектов, которые носят название поля, методы и свойства.
На физическом уровне рассматривается организация объектов в памяти, организация и вызов методов.
Классом называется особый тип записи, который может иметь в своём составе поля, методы и свойства. Такой тип также будем называть объектным типом. Ниже приводится описание класса TFrac, помещённое в модуль UFrac. В модуле размещают описание одного или нескольких логически связанных классов.
//--------------------------------------------------------------
unit UFrac;
interface
Uses SysUtils,Dialogs;
type
TFrac = class
FN,FD: Real;
constructor Create(Nr: Real = 1;Dr: Real = 1);
function Add(b: TFrac): TFrac;
function GetFrac: String;
end;
implementation
//--------------------------------------------------------------
constructor TFrac.Create(Nr,Dr: Real);
begin
FN:= Int(Nr);
FD:= Int(Dr);
end;
//--------------------------------------------------------------
function TFrac.add;
var c,e: Real;{c+d}
begin
c:= FN * b.FD + FD * b.FN;
e:= FD * b.FD;
result:= TFrac.Create(c,e);
end;
//--------------------------------------------------------------
function TFrac.GetFrac;
begin
Result:= FloatToStr(FN) + '/' + FloatToStr(FD);
end;
end.
//--------------------------------------------------------------
В примере описан класс TFrac, имеющий поля FN (числитель), FD (знаменатель) методы Add (сложить), GetFrac (представить в виде строки), Create (конструктор).
Где допустимо описание классов? Классы могут быть описаны:
Не допускается описание классов внутри процедур и других блоков кода. Разрешено опережающее описание классов, как в следующем примере:
//--------------------------------------------------------------
Type
TFClass = class;
TSClass = class(TObject)
First: TFClass;
…
End;
TFClass = class
…
end;
//--------------------------------------------------------------
Для того чтобы использовать новый тип в программе, нужно объявить переменную этого типа. Переменная объектного типа называется экземпляром класса, или объектом:
//--------------------------------------------------------------
program PExample;
uses SysUtils, UFrac;
Var
FracObj: TFrac;
//--------------------------------------------------------------
Для каждого класса имеется один набор методов, которыми пользуются все объекты (экземпляры) класса.
На уровне физической памяти объект состоит из полей, которые описаны в его объектовом типе и содержат данные, уникальные для каждого объекта класса. Кроме того, в состав объекта по умолчанию добавляется поле, для хранения указателя на структуру RTTI класса (указатель на класс). Ниже на рисунке показано распределение памяти под FracObj. В RTTI каждого класса имеется указатель на RTTI родительского класса.
Рисунок 1. Организация объектов в памяти.
Ограничений на тип полей в классе не предусмотрено. В описании класса поля должны предшествовать методам и свойствам. Обычно поля используются для хранения значений атрибутов объекта. Обработка полей объекта осуществляется операциями, которые представлены в описании класса как методы. При объявлении имен полей принято к названию поля добавлять заглавную букву F FSomeField.
В отличие от полей, методы являются общими для всех объектов класса. Методы – это процедуры или функции, описанные внутри класса и предназначенные для операций над полями. В описании класса как типа данных методы представлены своими заголовками. Полное описание методов осуществляется в разделе подпрограмм. Если класс описан в разделе типов раздела интерфейса (interface) модуля, то полное описание методов приводится в разделе описания подпрограмм раздела реализации (implementation) модуля. В процессе выполнения приложения класс представлен специальной структурой данных RTTI (run time type information) – информация о типе времени выполнения. В RTTI содержатся данные доступные приложению во время выполнения. В частности, там хранится имя класса, размер объекта, адреса памяти, по которым размещены в памяти коды методов.
От обычных подпрограмм методы отличаются, во-первых, тем что, в заголовке полного описания метода идентификатору метода предшествует идентификатор класса, отделённый от него точкой, во-вторых, тем, что им при вызове передаётся (неявно) указатель на тот объект, который их вызвал. Внутри метода он доступен под зарезервированным именем self.
Понятие свойство мы определим позже. Пока его можно рассматривать как поле, доступное не напрямую, а через методы.
Объекты в Delphi могут быть только динамическими, т. е. создаются в свободной памяти (куче). Описание в программе переменной типа класс не приводит к автоматическому распределению памяти под объект класса при запуске приложения. Переменная типа класс, например FracObj: TFrac, на самом деле, является указателем. Переменная FracObj используется для хранения указателя на объект её класса. За создание экземпляров класса и освобождение занимаемой ими памяти отвечает программист.
Использование объекта как указателя имеет одну особенность. При использовании указателя для доступа к данным, на которые он указывает, необходимо применять операцию разыменования “^”. Но это правило не распространяется на объекты. То есть вместо
FracObj^.FN:= 5;
Следует писать
FracObj.FN:= 5;
Новый экземпляр объекта создаётся конструктором, а уничтожается специальным методом – деструктором:
FracObj:= TFrac.Create;{Создание объекта}
…
FracObj.Destroy; {Удаление объекта}
Конструктор является классовым методом, поэтому может быть вызван до создания объекта. Для его вызова используется идентификатор типа. В Object Pascal у класса может быть несколько конструкторов. Принято называть конструктор Create. Для уничтожения объектов (освобождения памяти из под объектов) используют деструкторы. Типичное название деструктора – Destroy. В документации рекомендуется использовать для уничтожения экземпляра объекта метод Free, который первоначально проверяет указатель (не равен ли он nil) и затем уж вызывает Destroy.
Поскольку каждый класс по умолчанию является потомком класса Tobject, класс TMyClass унаследует от него методы: Create, Destroy, Free, описанные в классе Tobject.
Tobject = class
Constructor Create;
Destructor Destroy; virtual;
Procedure Free;
…
end;
Для того чтобы правильно проинициализировать в создаваемом объекте поля, относящиеся к классу предку, необходимо сразу же при входе в конструктор вызвать конструктор прямого предка:
Constructor TMyClass.Create;
Begin
inherited Create;
…
End;
Директива inherited позволяет вызвать одноименный метод прямого предка.
Конструктор можно использовать для инициализации полей уже созданного объекта. В этом случае объект должен вызвать конструктор. Конструктор установит в поля новые значения, которые необходимо передать в него через параметры. Конструктор по умолчанию без параметров, унаследованный от класса TObject, для этой цели не подходит. В классе необходимо описать собственный конструктор с параметрами. Так для класса TFrac возможно следующее использование конструктора:
FracObj:= TFrac.Create;{Создание объекта и инициализация полей значениями по умолчанию}
…
FracObj.Create(7,9);//Инициализация полей значениями 7 и 9.
Для правильного освобождения памяти, занимаемой объектом, перед выходом из деструктора необходимо вызвать деструктор прямого предка:
Destructor TMyClass.Destroy;
Begin
…
inherited Destroy;
End;
Пример 1. Работ с объектами класса TFrac в режиме консольного приложения.
program PFrac;
{$APPTYPE CONSOLE}
uses
SysUtils,
UFrac in 'UFrac.pas';
var a,b,c: TFrac;
begin
a:= TFrac.Create(-1,2);
writeln(a.getFrac);// -1/2
b:= TFrac.Create(3,4);// 3/4
writeln(b.getFrac);
c:= a.Add(b);
writeln(c.getFrac);// 2/8
c.Free;
a.FN:= -3;
a.FD:= 4;
c:= a.Add(b);
writeln(c.getFrac);// 0/16
readln;
end.
В Пример 1. Работ с объектами класса TFrac в режиме консольного приложения. выполняются следующие действия:
При работе с визуальными компонентами конструкторы и деструкторы вызываются редко. Поскольку за создание и уничтожение компонента отвечает компонент-владелец (owner). Для большинства компонентов это форма, на которую они помещены. За создание и уничтожение форм отвечает объект Application. Создавать объекты явно приходится только в том случае, если они создаются во время работы приложения (динамически).
Пример 2. Приложение из одной формы.
//--------------------------Модуль формы------------------------
unit UExample1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
begin
//Close;
//Form1.Close;
Application.Terminate;
end;
end.
//---------------Головная программа (source)--------------------
program PExample1;
uses
Forms,
UExample1 in 'UExample1.pas' {Form1};
{$R *.res}
begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
Каждый компонент, помещаемый на форму, становится полем класса формы. Каждый обработчик события для компонента становится методом формы. За создание и уничтожение формы отвечает объект Application. При закрытии формы в приложении, состоящем из одной формы, закрывается всё приложение.
Головная программа содержит три оператора:
ОПП основано на трёх принципах:
Правило ООП утверждает, что для обеспечения надёжности нежелателен прямой доступ к полям объекта: чтение и обновление их содержимого должно производиться посредством вызова соответствующих методов. Это правило и называется инкапсуляций. Для обеспечения инкапсуляции на уровне языка введена конструкция – свойство (property). В объектах Delphi пользователь объекта может быть полностью отгорожен от полей при помощи свойств.
Обычно свойство определяется тремя своими элементами: полем и двумя методами, которые осуществляют его чтение\запись:
//--------------------------------------------------------------
Type
//--------------------------------------------------------------
TFrac = class
FN,FD: Real;
function GetFrac: String;
procedure SetFrac(newn: String);
property Frac: String read GetFrac write SetFrac;
end;
//--------------------------------------------------------------
function TFrac.GetFrac;
var s: String;
begin
s:= FloatToStr(FN) + '/' + FloatToStr(FD);
Result:= s
end;
//--------------------------------------------------------------
procedure TFrac.SetFrac;
var
n: Integer;
z: String;
begin
try
n:= System.Pos('/',newn);
FN:= StrToInt(System.Copy(newn,1,(n - 1)));
Delete(newn,1,n);
FD:= StrToInt(System.Copy(newn,1,Length(newn)));
except
on EConvertError do ShowMessage('Ошибка ввода')
end;
end;
//--------------------------------------------------------------
В данном примере свойство Frac позволяет нам осуществлять запись и чтение простой дроби в формате строки. Причём с помощью свойства мы обрабатываем сразу оба поля объекта. Доступ к значению свойства Frac осуществляется через вызовы методов GetFrac, SetFrac. Однако для обращения к этим методам в явном виде нет необходимости достаточно написать:
//--------------------------------------------------------------
Var
Obj: TFrac;
S: String;
…
Obj.TFrac:= ‘1/2’;
S:= Obj.Frac;
//--------------------------------------------------------------
И компилятор транслирует эти операторы в вызовы соответствующих методов. То есть внешне свойство выглядит в точности как обычное поле, но за всяким обращением к нему стоят нужные вам действия. Например, если у вас есть объект, представляющий квадрат на экране, и вы его свойству “цвет” присваиваете значение “белый”, то произойдёт немедленная перерисовка, приводящая реальный цвет на экране в соответствие значению свойства.
В методах, входящих в состав свойств, может осуществляться проверка устанавливаемой величины на попадание в допустимый диапазон значений, и вызов других процедур, зависящих от вносимых изменений. Если же потребности в специальных процедурах записи\чтения нет, возможно, вместо имён методов применять имена полей. Рассмотрим следующую конструкцию:
//--------------------------------------------------------------
Type
TPropClass = class
FValue: TSomeType;
Procedure DoSomeThing;
function Correct(AValue: Integer): Boolean;
Procedure SetValue(NewValue: Integer);
Property AValue: Integer read FValue write SetValue;
End;
…
//--------------------------------------------------------------
procedure TPropClass.SetValue(NewValue: Integer);
begin
if (NewValue <> FValue) and Correct(NewValue)
then AValue:= NewValue;
DoSomeThing;
end;
//--------------------------------------------------------------
В этом примере чтение значения свойства AValue просто означает чтение поля FValue. Зато при присвоении ему значения внутри SetValue вызывается сразу два метода. Тип свойства и тип поля, на которое опирается свойство, могут быть разными.
Если свойство предназначено только для чтения или только для записи, в его описании может присутствовать только соответствующий метод:
//--------------------------------------------------------------
Type
TPropClass = class
FValue: Integer;
Property AValue: Integer read FValue;
End;
//--------------------------------------------------------------
В этом примере вне объекта свойство можно лишь прочитать; попытка присвоить AValue значение вызовет ошибку компиляции.
//--------------------------------------------------------------
Type
TPropClass = class
Property AValue: Integer write SetValue;
End;
//--------------------------------------------------------------
Свойство может быть и векторным; в этом случае оно выглядит как массив:
//--------------------------------------------------------------
Property APoint[index: Integer]: TPoint read GetPoint write SetPoint;
//--------------------------------------------------------------
На самом деле поле, на которое опирается это свойство, может и не являться массивом.
Для векторного свойства необходимо описать не только тип элементов массива, но также имя и тип индекса. И после read и после write в этом случае должны стоять имена методов – использование имени поля типа массив недопустимо.
Метод, читающий значение векторного свойства должен быть описан как функция, возвращающая значение того же типа, что и элементы свойства, и имеющая единственный параметр: того же типа и с тем же именем, что и индекс свойства:
//--------------------------------------------------------------
function GetPoint (index: Integer): TPoint;
//--------------------------------------------------------------
Аналогично, метод, помещающий значение в такое свойство, должен первым параметром иметь индекс, а вторым – переменную нужного типа (которая может быть передана как по ссылке, так и по значению):
//--------------------------------------------------------------
procedure SetPoint (index: Integer; NewPoint: TPoint);
//--------------------------------------------------------------
У векторных свойств есть ещё одна особенность. Некоторые классы могут иметь несколько векторных свойств. Основное свойство такого класса даёт доступ к некоторому массиву, а все остальные свойства являются вспомогательными. Специально для упрощения работы вектрное свойство может быть описано как default:
//--------------------------------------------------------------
Type
TMyClass = class
Property Strings[index: Integer]: String read Get write Put; default;
//--------------------------------------------------------------
Когда у объекта есть такое свойство, то его можно не упоминать, а ставить индекс в квадратных скобках прямо у имени объекта:
//--------------------------------------------------------------
Var AMyObject: TMyClass;
Begin
…
AMyObject.Strings[1]:= ‘First’;
AMyObject[2]:= ‘Second’;
…
end.
//--------------------------------------------------------------
О роли свойств в Delphi говорит тот факт, что у всех, имеющихся в вашем распоряжении стандартных классов100% полей недоступны и заменены базирующимися на них свойствами. При разработке своих собственных классов придерживайтесь этого же правила.
Пример 3. Работа с объектом простая дробьь через простое свойство
Класс простые дроби. Свойство позволяет читать и писать значение дроби в формате строки. Поля “числитель – FN” и “знаменатель - FD” и методы для чтения (getFrac) и записи (setFrac) помещены в раздел с уровнем доступа private (закрытый). Свойство Frac помещено в раздел с уровнем доступа public (открытый).
//--------------------------------------------------------------
program PFrac;
{$APPTYPE CONSOLE}
uses
SysUtils,
UFrac in 'UFrac.pas';
var a,b,c: TFrac;
begin
a:= TFrac.Create(-1,2);
writeln(a.Frac);// -1/2 вывод дроби через свойство в формате строки
b:= TFrac.Create(3,4);
c:= a.Add(b);
// ¾ вывод дроби через свойство в формате строкиwriteln(b.Frac);
writeln(c.Frac);// 2/8 вывод дроби через свойство в формате строки
c.Free;
a.Frac:= '-3/4';// ввод дроби через свойство в формате строки
c:= a.Add(b);
writeln(c.Frac);// 0/16 вывод дроби через свойство в формате строки
readln;
end.
//--------------------------------------------------------------
unit UFrac;
interface
Uses SysUtils,Dialogs;
type
TFrac = class
private
FN,FD: Real;
function GetFrac: String;
procedure SetFrac(newn: String);
public
constructor Create(Nr: Real = 1;Dr: Real = 1);
function Add(b: TFrac): TFrac;
property Frac: String read GetFrac write SetFrac;
end;
implementation
//--------------------------------------------------------------
constructor TFrac.Create(Nr,Dr: Real);
begin
FN:= Int(Nr);
FD:= Int(Dr);
end;
//--------------------------------------------------------------
function TFrac.add;
var c,e: Real;{c+d}
begin
c:= FN * b.FD + FD * b.FN;
e:= FD * b.FD;
result:= TFrac.Create(c,e);
end;
//--------------------------------------------------------------
function TFrac.GetFrac;
var s: String;
begin
s:= FloatToStr(FN) + '/' + FloatToStr(FD);
Result:= s
end;
//--------------------------------------------------------------
procedure TFrac.SetFrac;
var
n: Integer;
z: String;
begin
try
n:= System.Pos('/',newn);
FN:= StrToInt(System.Copy(newn,1,(n - 1)));
Delete(newn,1,n);
FD:= StrToInt(System.Copy(newn,1,Length(newn)));
except
on EConvertError do ShowMessage('Ошибка ввода')
end;
end;
//--------------------------------------------------------------
end.
Контрольные вопросы
Источники дополнительных сведений