Назад Домой! Дальше Лекция 14. (продолжение):
3.5.3 Конструкторы и деструкторы


Конструкторы - это специальные методы, создающие и инициализирующие объект класса. Объект создается выделением для него памяти в динамически распределяемой области памяти heap. Объявление конструктора выглядит так же, как объявление процедуры, но предваряется ключевым словом constructor. В качестве имени конструктора обычно задают имя Create.

В реализации конструктора обычно первым идет вызов наследуемого конструктора с помощью ключевого слова inherited. В результате инициализируются все наследуемые поля. При этом порядковым типам в качестве начального значения задается 0, указателям — nil, строки задаются пустыми. После вызова наследуемого конструктора в процедуре инициализируются новые поля, введенные в данном классе.

Не обязательно при создании класса объявлять его конструктор. Если он не объявлен, то автоматически в момент создания объекта вызовется конструктор родительского класса. Мы это видели в примере класса TPerson и его тестового приложения, созданных в разд. 3.5.2. Там мы обошлись без конструктора, и все работало нормально. Но давайте несколько расширим возможности нашего класса, и посмотрим, сможем ли мы обойтись без конструктора. Добавим в класс открытые поля AgeMin, AgeMax, обозначающие минимальную и максимальную границы допустимого возраста. Подобные границы полезны, если приложение использует наш класс для регистрации претендентов на какую-то должность при приеме на работу, или для регистрации абитуриентов при приеме в учебное заведение. Пусть при задании года рождения класс автоматически проверяет, удовлетворяет ли регистрируемая личность поставленным возрастным ограничениям. Для реализации этой идеи в объявлении класса надо произвести следующие изменения:

uses   ...,   DateUtils;
type
TPerson  = class
private

protected 
procedure SetYear(Value: word); // Процедура записи

public AgeMin, AgeMax:  word;
property Year: word read FYear write SetYear;
end;

В оператор uses вводится ссылка на модуль DateUtils, В этом модуле объявлены функции, которые мы будем использовать при записи года рождения. В защищенный раздел класса protected вводится объявление процедуры записи года рождения. В открытый раздел класса вводятся переменные AgeMin и AgeMax. И изменяется объявление свойства Year: теперь в него вводится ссылка на процедуру записи SetYear.
Реализация процедуры записи SetYear может быть следующей:

procedure TPerson.SetYear(Value:  word); //  Процедура записи года рождения 
var NowYear: word;
begin
  NowYear:= YearOf(Date);
  if (NowYear - Value >= AgeMin) and (NowYear - Value <= AgeMax) then
    FYear:= Value else ShowMessage('Недопустимый год рождения '+IntToStr(Value)); 
 end;

В переменную NowYear заносится текущий год. Делается это так. Вызывается функция Date, которая возвращает текущую дату. Затем к этому результату применяется функция YearOf, которая выделяет из даты год. Функция YearOf объявлена в модуле DateUtils. Именно из-за нее этот модуль подключается предложением uses.

Остальное просто: проверяется, укладывается ли заданный год рождения Value в заданный диапазон, и если не укладывается, пользователю выдается соответствующее сообщение.

Возможно, вы уже заметили, почему все это пока не будет нормально работать. Если нет, введите описанные изменения в ваш класс и выполните тестовое приложение. Вы увидите, что можете занести в объект класса только того, кто родился в текущем году. А такой гражданин, пожалуй, слишком молод для приема на работу или учебу. Объясняется это просто: целые поля, такие как AgeMin и AgeMax, инициализируются нулевыми значениями. А нам нужны какие-то другие, более разумные возрастные рамки. Конечно, пользователь класса легко может этот недостаток устранить. Достаточно ввести, например, в обработчик события формы OnCreate после оператора создания объекта класса операторы вида:

Pers.AgeMax := 45;
Pers.AgeMin := 16;

Тем самым пользователь задает возрастные рамки для своего приложения. Ведь открытые поля AgeMin и AgeMax мы для того и вводили. Но хотелось бы, чтобы в случаях, если пользователю не требуется какой-то специфический возрастной диапазон, класс позволял бы работать с любыми реальными годами рождения. Но именно реальными, чтобы предотвратить случайную ошибку пользователя, когда он задаст, например, год рождения 3 или 3003.

Как указывалось в разд. 3.5.3, задать начальные значения полей в их объявлениях невозможно. Вот для этого и служит конструктор класса. Добавьте в открытый раздел вашего класса объявление:

constructor Create;

А в раздел implementation добавьте реализацию конструктора:

constructor TPerson.Create; 
const Unknown = 'неизвестный'; 
begin
inherited;
FSex := #0;
AgeMax := 150;
FName := Unknown;
FDepl := Unknown;
FDep2 := Unknown;
FDep3 := Unknown; 
end;

Первый оператор вызывает с помощью ключевого слова inherited наследуемый конструктор родительского класса. А затем задаются начальные значения различных полей. Кроме задания максимального возраста 150 лет, тут исправляются еще некоторые недостатки нашего класса. Задается значение пола равное нулевому символу. По такому значению в дальнейшем можно проверять, был ли указан пол личности. И задаются строки «неизвестный» в качестве начального значения строковых нолей. Так что теперь, если какие-то данные о личности неизвестны, вместо них будет выдаваться этот текст, а не пустая строка. Думаю, что пользователю это будет удобно.

Теперь рассмотрим деструкторы. Это специальные методы, уничтожающие объект и освобождающие занимаемую им память. Деструктор автоматически вызывается при выполнении метода Free объекта класса. Если в вашем классе деструктор не объявлен, вызывается деструктор родительского класса.

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

Давайте введем в наш класс TPerson еще одно добавление, которое расширит его возможности. Создадим возможность хранить форматированный текст в формате .rtf, который может поступать, как вы знаете, из окна RichEdit (см. разд. 3.2.4). Но форматированный текст можно хранить или в объекте класса TRichEdit, или в файле, в который он записан методом SaveToFile свойства Lines окна RichEdit. Создать в нашем классе внутренний объект класса TRichEdit мы не можем. Так что остается вариант хранения во временном файле.
Добавьте в класс TPerson два поля:

FDoc: ^TRichEdit;
FileTMP: string;

Поле FDoc будет служить указателем на внешний компонент класса TRichEdit, из которого будет загружаться форматированный текст в файл, и в который будет грузиться текст из файла. А в переменной FileTmp будем хранить имя временного файла. Чтобы компилятор принял объявление поля FDoc, он должен понять идентификатор TRichEdit. Этот класс объявлен в модуле ComCtrls. Так что добавьте ссылку на этот модуль в предложение uses.
Введите в открытый раздел класса объявления двух процедур:

procedure SetDoc(var Value: TRichEdit); procedure GetDoc(var Value: TRichEdit);

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

procedure TPerson.SetDoc(var Value: TRichEdit); 
begin
  if   FileTMP = ''
    then FileTMP:= FName +‘.tmp’;
  FDoc:= @Value;
  FDoc^.Lines.SaveToFile(FileTMP); 
end;

procedure TPerson.GetDoc(var Value: TRichEdit); 
begin 
  if FileTMP <> ''
    then FDoc^.Lines.LoadFromFile(FileTMP); 
end;

В процедуре SetDoc сначала проверяется, задавалось ли уже имя временного файла. Если нет, то это имя формируется из строки FName (фамилия, имя, отчество) и расширения .tmp. Затем переменной FDoc задается адрес компонента TRichEdit. А дальнейшее понятно: форматированный текст записывается в файл с именем, хранящимся в переменной FileTMP.

Добавьте в свое тестовое приложение окно RichEdit. Можете добавить также компонент FontDialog и обеспечить возможность форматирования текста в окне RichEdit (см. разд. 3.2.4). В обработчик щелчка на кнопке Запись вставьте оператор:

SetDoc(RichEditl);

А в обработчик щелчка на кнопке Чтение вставьте оператор:

GetDoc(RichEditl);

Выполнив тестовое приложение, можете убедиться, что ваш класс стал намного мощнее и может теперь хранить форматированный текст. Но он пока не совсем правильно оформлен. Если в приложении выполнялся вызов процедуры SetDoc, то был создан временный файл. И когда приложение завершается, этот файл остается на диске. Вам нужно написать деструктор, который удалял бы этот файл.

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

В нашем случае в открытый раздел класса следует ввести объявление деструктора:

destructor Destroy; override;

Смысл ключевого слова override вы узнаете позднее. Пока просто напишите его, не задумываясь о его назначении. Реализация деструктора имеет вид:

destructor  TPerson.Destroy;	
begin
if   FileTMP  <> ' ' then DeleteFile(FileTMP);
inherited; 
end;
Если файл создавался, то он уничтожается. Введение такого деструктора никак не отразится на внешнем поведении вашего тестового приложения. Но теперь вы не оставляете на диске временный и уже ненужный файл.

3.5.4 Методы, наследование классов, операции с классами

В предыдущих разделах мы уже создали ряд методов: чтения и записи свойств, конструктор, деструктор. Поскольку вы уже освоились с методами, давайте создадим еще два метода: SaveToFile и LoadFromFile, позволяющих сохранять сведения о личности в файле и читать их из файла.

Без подобных методов наш класс явно неполноценный. Введите в класс объявления открытых методов:

procedure SaveToFile(FileName: string  = '');
procedure LoadFromFile(FileName:   string);
Первый метод содержит значение параметра по умолчанию (см. разд. 2.7.3). Предполагается, что если при вызове метода параметр не задан, то имя файла бу-.дет совпадать с именем личности, записанным в поле FName. Реализация объявленных методов может быть такой:
procedure  TPerson.SaveToFile(FileName: string  = '');
var  F:   TextFile;
begin
if FileName = '' then FileName := FName + '.txt'
else FileName := ChangeFileExt (FileName, '.txt');
AssignFile(F, FileName);
Rewrite(F);
Writeln(F, FName);
Writeln(F, FDepl) ;
Writeln(F, FDep2) ;
Writeln(F, FDep3);
Writeln(F,    IntToStr(FYear)) ;    .
Writeln(F,   FSex);
if  FAttr   then  Writeln(F, 't')  else Writeln(F, 'f') ;
Writeln(F,   FComment);
CloseFile(F);
if FDoc <> nil
then  FDoc^.Lines.SaveToFile(ChangeFileExt(FileName, '.rtf')); end;
procedure   TPerson.LoadFromFile(FileName:   string);
 var  F:   TextFile;
s:   string; begin
FileName   :=  ChangeFileExt(FileName,  '.txt'); AssignFile(F,   FileName); Reset(F);
Readln(F, FName); Readln(F, FDepl); 
Readln(F, FDep2); Readln(F, FDep3); 
Readln(F, s) ; 
FYear   :=   StrToInt(s); Readln(F, FSex); Readln(F, s) ; FAttr :=   (s  =  ' t' ) ; 
Readln(F,   FComment); CloseFile(F); 
if   (FDoc  <>  nil)   and  
FileExists(ChangeFileExt(FileName, '.rtf)) then  begin
    FDoc^.Lines.LoadFromFile(ChangeFileExt(FileName, '.rtf'));  
    FDoc^.Lines.SaveToFile(FileTMP); 
  end;
 end;

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

Так что может иметь смысл хотя бы зашифровать его в процедуре SaveToFile и дешифровать в процедуре LoadFromFile. Шифровка текстов рассмотрена в разд. 2.8.7.2. Вызов метода SaveToFile в вашем тестовом приложении может быть оформлен оператором:

Pers.SaveToFile;

ИЛИ

if SaveDialogl.Execute then Pers.SaveToFile(SaveDialogl.FileName);

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

Вызов метода LoadFromFile в вашем тестовом приложении может быть оформлен оператором:

if OpenDialogl.Execute then Pers.LoadFromFile(OpenDialogl.FileName);

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

TStudent = class(TPerson)
 public
  function PersonToStr: string;
 end;

TEmpl = class(TPerson)
 public
  function PersonToStr: string;
end;

Эти классы наследуют все свойства и методы своего базового класса TPerson. В обоих классах объявлены новые открытые методы PersonToStr. Предполагается, что их назначение - представить информацию о человеке в виде единой строки. Такая строка полезна во многих случаях. В частности, например, для подготовки шапки характеристики, как в приложении, которое вы разрабатывали в разд. 1.3.
Реализация этих методов может иметь вид:

function TStudent.PersonToStr: string; 
const Unknown = 'неизвестный'; var S: string;
begin
S := FName + ' , '; 
  if FYear = 0 then S := S + Unknown 
    else S := S + IntToStr(FYear); 
S : = S + ' г.p., ' ; 
if FSex = 'ж’ then S := S + 'студентка' 
else S := S + 'студент';
 S := S + ' группы ' + FDepl + ' института ' + FDep2;  
 Result := S; 
end;

function TEmpl.PersonToStr: string; 
const Unknown = 'неизвестный'; var S: string; 
begin
S := FName + ', '; 
if FYear = 0 then S := S + Unknown 
  else 
S := S + IntToStr(FYear);
S := S + ' r.p., ';
 if FSex = 'ж' then 
   S := S + 'сотрудница' 
   else S := S + 'сотрудник'; 
S := S + ' отдела ' + FDepl + ' организации ' +
FDep2;
Result := S; 
end;

Комментарии к этому коду, наверное, излишни. Имеет смысл сказать только о смысле свойств Depl и Dep2 в новых классах. Как видно из кода, свойство Depl в классе TStudent указывает студенческую группу, а в классе TEmpl - отдел. Свойство Dep2 указывает организацию (институт), в которой работает (учится) человек. Вы можете, конечно, воспринимать свойства методов иначе и соответственно переделать приведенные функции.

Можете добавить в ваше тестовое приложение еще одно окно Edit для задания свойства Dep2. Но главное сейчас - посмотреть соотношение родительского и производных классов.
Можете оставить прежнее объявление
var Pers: TPerson; но при создании объекта указать какой-то производный класс, например:

Pers := TStudent.Create;

Все будет работать, т.е. производный класс присвоится переменной родительского класса. Но если вы попробуете записать оператор вида:
ShowMessage(Pers.PersonToStr);
то получите сообщение об ошибке:

«Undeclared identifier: 'PersonToStr'»,

поскольку в классе TPerson не объявлен метод PersonToStr. Но все будет нормально, если вы измените этот оператор следующим образом:

ShowMessage((Pers as TStudent).PersonToStr);

В этом операторе вы используете операцию as, определенную для классов. Ее первым операндом является объект, вторым - класс. Если А — объект, а С -класс, то выражение A as С возвращает тот же самый объект, но рассматриваемый как объект класса С (В смысле: у объекта А будут свойства и методы объекта С). Операция даст результат, если указанный класс С является классом объекта А или одним из наследников этого класса. В нашем случае это условие соблюдается, так что операция as возвращает объект Pers, рассматриваемый как объект TStudent. А в таком объекте метод PersonToStr имеется. Если бы вы сразу объявили переменную Pers класса TStudent:

var Pers: TStudent;

то операция as не потребовалась бы. Выражение Pers.PersonToStr было бы воспринято нормально.

Для классов определена еще одна операция - is. Выражение A is С позволяет определить, относится ли объект А к классу С или к одному из его потомков. Если относится, то операция is возвращает true, в противном случае - false. Например, в нашем примере при любом объявлении класса создаваемого объекта Pers выражение Pers is TPerson вернет true. Но выражение Pers is TStudent вернет true, если объект создан как объект TStudent, и вернет false, если объект создан как объект TPerson.

Теперь посмотрим наследование методов. Давайте введем метод PersonToStr не только в производные, но и в базовый класс TPerson. Для этого класса его реализация может быть очень простой:

function TPerson.PersonToStr: string; begin Result := FName; end;

Теперь метод PersonToStr объявлен с одним и тем же именем и в родительском, и в производных классах. Иначе говоря, вы переопределили или перегрузили родительский метод в производных классах. Теперь вы сможете увидеть, что выражение Pers.PersonToStr будет всегда нормально срабатывать.

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

TPerson(Pers).PersonToStr

При реализации метода, переопределенного любым способом в классе-наследнике, можно вызывать метод класса-родителя. Для этого перед именем метода при его вызове записывается ключевое слово inherited. Например, оператор

inherited PersonToStr;

вызывает метод PersonToStr родительского класса.

Если записать слово inherited и после него не писать имя вызываемого метода, то будет вызываться наследуемый метод, совпадающий по имени с именем того метода, из которого он вызывается. Например, если в переопределяемом конструкторе встречается оператор inherited; то будет вызван конструктор родительского класса. Этим мы уже пользовались в разд. 3.5.3 при написании конструкторов и деструкторов класса TPerson.

Окончание далее.

Назад Дальше

Rambler's Top100
Hosted by uCoz