Файл: Учебное пособие для студентов Авторы А. Н. Вальвачев, К. А. Сурков, Д. А. Сурков, Ю. М. Четырько Содержание Содержание 1.doc

ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 04.05.2024

Просмотров: 158

Скачиваний: 4

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
несвязанной и не должна использоваться для доступа к полям и методам уже несуществующего объекта. Чтобы отличать в программе связанные объектные переменные от несвязанных, последние следует инициализировать значением nil. Например, в следующем фрагменте обращение к деструктору Destroy выполняется только в том случае, если объект реально существует:

Reader := nil;

...

if Reader <> nil then Reader.Destroy;

Вызов деструктора для несуществующих объектов недопустим и при выполнении программы приведет к ошибке. Чтобы избавить программистов от лишних ошибок, в объекты ввели предопределенный метод Free, который следует вызывать вместо деструктора. Метод Free сам вызывает деструктор Destroy, но только в том случае, если значение объектной переменной не равно nil. Поэтому последнюю строчку в приведенном выше примере можно переписать следующим образом.

Reader.Free;

После уничтожения объекта переменная Reader сохраняет свое значение, продолжая ссылаться на место в памяти, где объекта уже нет. Если эту переменную предполагается еще использовать, то желательно присвоить ей значение nil, чтобы программа могла проверить, существует объект или нет. Таким образом, наиболее правильная последовательность действий при уничтожении объекта должна быть следующая:

Reader.Free;

Reader := nil;

С помощью стандартной процедуры FreeAndNil это можно сделать проще и элегантнее:

FreeAndNil(Reader); // Эквивалентно: Reader.Free; Reader := nil;

Значение одной объектной переменной можно присвоить другой. При этом объект не копируется в памяти, а вторая переменная просто связывается с тем же объектом, что и первая:

var

R1, R2: TDelimitedReader; // Переменные R1 и R2 не связаны с объектом

begin

R1 := TDelimitedReader.Create; // Связывание переменной R1 с новым объектом

// Переменная R2 пока еще не связана ни с каким объектом

R2 := R1; // Связывание переменной R2 с тем же объектом, что и R1

// Теперь обе переменные связаны с одним объектом

R2.Free; // Уничтожение объекта

// Теперь R1 и R2 не связаны ни с каким объектом

end;

Объекты могут выступать в программе не только в качестве переменных, но также элементов массивов, полей записей, параметров процедур и функций. Кроме того, они могут служить полями других объектов. Во всех этих случаях программист фактически оперирует указателями на экземпляры объектов в динамической памяти. Следовательно, объекты изначально приспособлены для создания сложных динамических структур данных, таких как списки и деревья. Указатели на объекты для этого не нужны.


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

type

TReadersList = class; // упреждающее объявление класса TReadersList
TDelimitedReader = class

Owner: TReadersList;

...

end;
TReadersList = class

Readers: array of TDelimitedReader;

...

end;

Первое объявление класса TReadersList называется упреждающим (от англ. forward). Оно необходимо для того, чтобы компилятор нормально воспринял объявление поля Owner в классе TDelimitedReader.

Итак, вы уже имеете некоторое представление об объектах, перейдем теперь к вопросу реализации их методов.

3.4. Конструкторы и деструкторы

Особой разновидностью методов являются конструкторы и деструкторы. Напомним, что конструкторы создают, а деструкторы разрушают объекты. Создание объекта включает выделение памяти под экземпляр и инициализацию его полей, а разрушение — очистку полей и освобождение памяти. Действия по инициализации и очистке полей специфичны для каждого конкретного класса объектов. По этой причине язык Delphi позволяет переопределить стандартный конструктор Create и стандартный деструктор Destroy для выполнения любых полезных действий. Можно даже определить несколько конструкторов и деструкторов (имена им назначает сам программист), чтобы обеспечить различные процедуры создания и разрушения объектов.

Объявление конструкторов и деструкторов похоже на объявление обычных методов с той лишь разницей, что вместо зарезервированных слов function и procedure используются слова constructor и destructor. Для нашего класса TDelimitedReader потребуется конструктор, которому в качестве параметра будет передаваться имя обрабатываемого файла и разделитель элементов:

type

TDelimitedReader = class

...

// Конструкторы и деструкторы

constructor Create(const FileName: string; const ADelimiter: Char = ';');

destructor Destroy; override;

...

end;

Приведем их возможную реализацию:

constructor TDelimitedReader.Create(const FileName: string;

const ADelimiter: Char = ';');

begin

AssignFile(FileVar, FileName);

Delimiter := ADelimiter;

end;
destructor TDelimitedReader.Destroy;

begin

// Пока ничего не делаем

end;

Если объект содержит встроенные объекты или другие динамические данные, то конструктор — это как раз то место, где их нужно создавать.

Конструктор применяется к классу или к объекту. Если он применяется к классу,


Reader := TDelimitedReader.Create('MyData.del', ';');

то выполняется следующая последовательность действий:

  • в динамической памяти выделяется место для нового объекта;

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

  • затем выполняются заданные программистом действия конструктора;

  • ссылка на созданный объект возвращается в качестве значения конструктора. Тип возвращаемого значения совпадает с типом класса, использованного при вызове (в нашем примере это тип TDelimitedReader).

Если конструктор применяется к объекту,

Reader.Create('MyData.del', ';');

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

Деструктор уничтожает объект, к которому применяется:

Reader.Destroy;

В результате:

  • выполняется заданный программистом код завершения;

  • освобождается занимаемая объектом динамическая память.

В теле деструктора обычно должны уничтожаться встроенные объекты и динамические данные, как правило, созданные конструктором.

Как и обычные методы, деструктор может иметь параметры, но эта возможность используется редко.

3.5. Методы

Процедуры и функции, предназначенные для выполнения над объектами действий, называются методами. Предварительное объявление методов выполняется при описании класса в секции interface модуля, а их программный код записывается в секции implementation. Однако в отличие от обычных процедур и функций заголовки методов должны иметь уточненные имена, т.е. содержать наименование класса. Приведем возможную реализацию одного из методов в классе TDelimitedReader:

procedure TDelimitedReader.SetActive(const AActive: Boolean);

begin

if AActive then

Reset(FileVar) // Открытие файла

else

CloseFile(FileVar); // Закрытие файла

end;


Обратите внимание, что внутри методов обращения к полям и другим методам выполняются как к обычным переменным и подпрограммам без уточнения экземпляра объекта. Такое упрощение достигается путем использования в пределах метода псевдопеременной Self (стандартный идентификатор). Физически Self представляет собой дополнительный неявный параметр, передаваемый в метод при вызове. Этот параметр и указывает экземпляр объекта, к которому данный метод применяется. Чтобы пояснить сказанное, перепишем метод SetActive, представив его в виде обычной процедуры:

procedure TDelimitedReader_SetActive(Self: TDelimitedReader;

const AActive: Boolean);

begin

if AActive then

Reset(Self.FileVar) // Открытие файла

else

CloseFile(Self.FileVar); // Закрытие файла

end;

Согласитесь, что метод SetActive выглядит лаконичнее процедуры TDelimitedReader_SetActive.

Практика показывает, что псевдопеременная Self редко используется в явном виде. Ее необходимо применять только тогда, когда при написании метода может возникнуть какая-либо двусмысленность для компилятора, например при использовании одинаковых имен и для локальных переменных, и для полей объекта.

Если выполнить метод SetActive,

Reader.SetActive(True);

то обрабатываемый файл будет открыт. При этом неявный параметр Self будет содержать значение переменной Reader. Такой вызов реализуется обычными средствами процедурного программирования приблизительно так:

TDelimitedReader_SetActive(Reader, True);

3.6. Свойства

3.6.1. Понятие свойства

Помимо полей и методов в объектах существуют свойства. При работе с объектом свойства выглядят как поля: они принимают значения и участвуют в выражениях. Но в отличие от полей свойства не занимают места в памяти, а операции их чтения и записи ассоциируются с обычными полями или методами. Это позволяет создавать необходимые сопутствующие эффекты при обращении к свойствам. Например, в объекте Reader присваивание свойству Active значения True вызовет открытие файла, а присваивание значения False — закрытие файла. Создание сопутствующего эффекта (открытие или закрытие файла) достигается тем, что за присваиванием свойству значения стоит вызов метода.

Объявление свойства выполняется с помощью зарезервированного слова property, например:

type

TDelimitedReader = class

...

FActive: Boolean;

...

// Метод записи (установки значения) свойства

procedure SetActive(const AActive: Boolean);


property Active: Boolean read FActive write SetActive; // Свойство

end;

Ключевые слова read и write называются спецификаторами доступа. После слова read указывается поле или метод, к которому происходит обращение при чтении (получении) значения свойства, а после слова write — поле или метод, к которому происходит обращение при записи (установке) значения свойства. Например, чтение свойства Active означает чтение поля FActive, а установка свойства — вызов метода SetActive. Чтобы имена свойств не совпадали с именами полей, последние принято писать с буквы F (от англ. field). Мы в дальнейшем также будем пользоваться этим соглашением. Начнем с того, что переименуем поля класса TDelimitedReader: поле FileVar переименуем в FFile, Items — в FItems, а поле Delimiter — в FDelimiter.

type

TDelimitedReader = class

// Поля

FFile: TextFile; // FileVar -> FFile

FItems: array of string; // Items -> FItems

FActive: Boolean;

FDelimiter: Char; // Delimiter -> FDelimiter

...

end;

Обращение к свойствам выглядит в программе как обращение к полям:

var

Reader: TDelimitedReader;

IsOpen: Boolean;

...

Reader.Active := True; // Эквивалентно Reader.SetActive(True);

IsOpen := Reader.Active; // Эквивалентно IsOpen := Reader.FActive

Если один из спецификаторов доступа опущен, то значение свойства можно либо только читать (задан спецификатор read), либо только записывать (задан спецификатор write). В следующем примере объявлено свойство, значение которого можно только читать.

type

TDelimitedReader = class

...

FItems: array of string;

...

function GetItemCount: Integer;

...

property ItemCount: Integer read GetItemCount; // Только для чтения!

end;
function TDelimitedReader.GetItemCount: Integer;

begin

Result := Length(FItems);

end;

Здесь свойство ItemCount показывает количество элементов в массиве FItems. Поскольку оно определяется в результате чтения и разбора очередной строки файла, пользователю объекта разрешено лишь узнавать количество элементов.

В отличие от полей свойства не имеют адреса в памяти, поэтому к ним запрещено применять операцию @. Как следствие, их нельзя передавать в var- и out-параметрах процедур и функций.

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