Конструкторы - это специальные методы, создающие и инициализирующие объект класса. Объект создается выделением для него памяти в динамически распределяемой области памяти 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;Если файл создавался, то он уничтожается. Введение такого деструктора никак не отразится на внешнем поведении вашего тестового приложения. Но теперь вы не оставляете на диске временный и уже ненужный файл.
В предыдущих разделах мы уже создали ряд методов: чтения и записи свойств, конструктор, деструктор. Поскольку вы уже освоились с методами, давайте создадим еще два метода: 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.
Окончание далее.