Главная страница

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


Скачать 2.61 Mb.
НазваниеУчебное пособие для студентов Авторы А. Н. Вальвачев, К. А. Сурков, Д. А. Сурков, Ю. М. Четырько Содержание Содержание 1
Дата15.01.2023
Размер2.61 Mb.
Формат файлаdoc
Имя файлаlab_OOTP.doc
ТипУчебное пособие
#886736
страница13 из 33
1   ...   9   10   11   12   13   14   15   16   ...   33

Create — стандартный конструктор.

Free — уничтожает объект: вызывает стандартный деструктор Destroy, если значение псевдопеременной Self не равно nil.

InitInstance(Instance: Pointer): TObject — при создании объекта инициализирует нулями выделенную память. На практике нет необходимости вызывать этот метод явно.

CleanupInstance — освобождает память, занимаемую полями с типом string, Variant, динамический массив и интерфейс. На практике нет необходимости вызывать этот метод явно.

ClassType: TClass — возвращает описатель класса (метакласс).

ClassName: ShortString — возвращает имя класса.

ClassNameIs(const Name: string): Boolean — проверяет, является ли заданная строка именем класса.

ClassParent: TClass — возвращает описатель базового класса.

ClassInfo: Pointer — возвращает указатель на соответствующую классу таблицу RTTI (от англ. Runtime Type Information). Таблица RTTI используется для проверки типов данных на этапе выполнения программы.

InstanceSize: Longint — возвращает количество байт, необходимых для хранения в памяти одного объекта соответствующего класса. Заметим, что значение, возвращаемое этим методом и значение, возвращаемое функцией SizeOf при передаче ей в качестве аргумента объектной переменной — это разные значения. Функция SizeOf всегда возвращает значение 4 (SizeOf(Pointer)), поскольку объектная переменная — это ни что иное, как ссылка на данные объекта в памяти. Значение InstanceSize — это размер этих данных, а не размер объектной переменной.

InheritsFrom(AClass: TClass): Boolean — проверяет, является ли класс AClass базовым классом.

MethodAddress(const Name: ShortString): Pointer — возвращает адрес published-метода, имя которого задается параметром Name.

MethodName(Address: Pointer): ShortString — возвращает имя published-метода по заданному адресу.

FieldAddress(const Name: ShortString): Pointer — возвращает адрес published-поля, имя которого задается параметром Name.

GetInterface(const IID: TGUID; out Obj): Boolean — возвращает ссылку на интерфейс через параметр Obj; идентификатор интерфейса задается параметром IID. (Интерфейсы рассмотрены в главе 6)

GetInterfaceEntry(const IID: TGUID): PInterfaceEntry — возвращает информацию об интерфейсе, который реализуется классом. Идентификатор интерфейса задается параметром IID.

GetInterfaceTable: PInterfaceTable — возвращает указатель на таблицу с информацией обо всех интерфейсах, реализуемых классом.

AfterConstruction — автоматически вызывается после создания объекта. Метод не предназначен для явного вызова из программы. Используется для того, чтобы выполнить определенные действия уже после создания объекта (для этого его необходимо переопределить в производных классах).

BeforeDestruction — автоматически вызывается перед уничтожением объекта. Метод не предназначен для явного вызова из программы. Используется для того, чтобы выполнить определенные действия непосредственно перед уничтожением объекта (для этого его необходимо переопределить в производных классах).

Dispatch(var Message) — служит для вызова методов, объявленных с ключевым словом message.

DefaultHandler(var Message) — вызывается методом Dispatch в том случае, если метод, соответствующий сообщению Message, не был найден.

NewInstance: TObject — вызывается при создании объекта для выделения динамической памяти, чтобы разместить в ней данные объекта. Метод вызывается автоматически, поэтому нет необходимости вызывать его явно.

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

Destroy — стандартный деструктор.

3.7.3. Перекрытие атрибутов в наследниках

В механизме наследования можно условно выделить три основных момента:

  • наследование полей;

  • наследование свойств;

  • наследование методов.

Любой порожденный класс наследует от родительского все поля данных, поэтому классы TDelimitedReader и TFixedReader автоматически содержат поля FFile, FActive и FItems, объявленные в классе TTextReader. Доступ к полям предка осуществляется по имени, как если бы они были определены в потомке. В потомках можно определять новые поля, но их имена должны отличаться от имен полей предка.

Наследование свойств и методов имеет свои особенности.

Свойство базового класса можно перекрыть (от англ. override) в производном классе, например чтобы добавить ему новый атрибут доступа или связать с другим полем или методом.

Метод базового класса тоже можно перекрыть в производном классе, например чтобы изменить логику его работы. Обратимся к классам TDelimitedReader и TFixedReader. В них методы PutItem, GetItem, SetActive и GetEndOfFile унаследованы от TTextReader, поскольку логика их работы не зависит от того, в каком формате хранятся данные в файле. А вот метод ParseLine перекрыт, так как способ разбора строк зависит от формата данных:

function TDelimitedReader.ParseLine(const Line: string): Integer;

var

S: string;

P: Integer;

begin

S := Line;

Result := 0;

repeat

P := Pos(Delimiter, S); // Поиск разделителя

if P = 0 then // Если разделитель не найден, то считается, что

P := Length(S) + 1; // разделитель находится за последним символом

PutItem(Result, Copy(S, 1, P - 1)); // Установка элемента

Delete(S, 1, P); // Удаление элемента из строки

Result := Result + 1; // Переход к следующему элементу

until S = ''; // Пока в строке есть символы

end;

function TFixedReader.ParseLine(const Line: string): Integer;

var

I, P: Integer;

begin

P := 1;

for I := 0 to High(FItemWidths) do

begin

PutItem(I, Copy(Line, P, FItemWidths[I])); // Установка элемента

P := P + FItemWidths[I]; // Переход к следующему элементу

end;

Result := Length(FItemWidths); // Количество элементов постоянно

end;

В классах TDelimitedReader и TFixedReader перекрыт еще и конструктор Create. Это необходимо для инициализации специфических полей этих классов (поля FDelimiter в классе TDelimitedReader и поля FItemWidths в классе TFixedReader):

constructor TDelimitedReader.Create(const FileName: string;

const ADelimiter: Char = ';');

begin

inherited Create(FileName);

FDelimiter := ADelimiter;

end;
constructor TFixedReader.Create(const FileName: string;

const AItemWidths: array of Integer);

var

I: Integer;

begin

inherited Create(FileName);

// Копирование AItemWidths в FItemWidths

SetLength(FItemWidths, Length(AItemWidths));

for I := 0 to High(AItemWidths) do

FItemWidths[I] := AItemWidths[I];

end;

Как видно из примера, в наследнике можно вызвать перекрытый метод предка, указав перед именем метода зарезервированное слово inherited. Когда метод предка полностью совпадает с методом потомка по формату заголовка, то можно использовать более короткую запись. Воспользуемся ей и перепишем деструктор в классе TTextReader правильно:

destructor TTextReader.Destroy;

begin

Active := False;

inherited; // Эквивалентно: inherited Destroy;

end;

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

3.7.4. Совместимость объектов различных классов

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

var

Reader: TTextReader;

...

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

Объектная переменная Reader формально имеет тип TTextReader, а фактически связана с экземпляром класса TDelimitedReader.

Правило совместимости классов чаще всего применяется при передаче объектов в параметрах процедур и функций. Например, если процедура работает с объектом класса TTextReader, то вместо него можно передать объект класса TDelimitedReader или TFixedReader.

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

3.7.5. Контроль и преобразование типов

Поскольку реальный экземпляр объекта может оказаться наследником класса, указанного при описании объектной переменной или параметра, бывает необходимо проверить, к какому классу принадлежит объект на самом деле. Чтобы программист мог выполнять такого рода проверки, каждый объект хранит информацию о своем классе. В языке Delphi существуют операторы is и as, с помощью которых выполняется соответственно проверка на тип (type checking) и преобразование к типу (type casting).

Например, чтобы выяснить, принадлежит ли некоторый объект Obj к классу TTextReader или его наследнику, следует использовать оператор is:

var

Obj: TObject;

...

if Obj is TTextReader then ...

Для преобразования объекта к нужному типу используется оператор as, например

with Obj as TTextReader do

Active := False;

Стоит отметить, что для объектов применим и обычный способ приведения типа:

with TTextReader(Obj) do

Active := False;

Вариант с оператором as лучше, поскольку безопасен. Он генерирует ошибку (точнее исключительную ситуацию; об исключительных ситуациях мы расскажем в главе 4) при выполнении программы (run-time error), если реальный экземпляр объекта Obj не совместим с классом TTextReader. Забегая вперед, скажем, что ошибку приведения типа можно обработать и таким образом избежать досрочного завершения программы.

3.8. Виртуальные методы

3.8.1. Понятие виртуального метода

Все методы, которые до сих пор рассматривались, имеют одну общую черту — все они статические. При обращении к статическому методу компилятор точно знает класс, которому данный метод принадлежит. Поэтому, например, обращение к статическому методу ParseLine в методе NextLine (принадлежащем классу TTextReader) компилируется в вызов TTextReader.ParseLine:

function TTextReader.NextLine: Boolean;

var

S: string;

N: Integer;

begin

Result := not EndOfFile;

if Result then

begin

Readln(FFile, S);

N := ParseLine(S); // Компилируется в вызов TTextReader.ParseLine(S);

if N <> ItemCount then

SetLength(FItems, N);

end;

end;

В результате метод NextLine работает неправильно в наследниках класса TTextReader, так как внутри него вызов перекрытого метода ParseLine не происходит. Конечно, в классах TDelimitedReader и TFixedReader можно продублировать все методы и свойства, которые прямо или косвенно вызывают ParseLine, но при этом теряются преимущества наследования, и мы возвращаемся к тому, что необходимо описать два класса, в которых большая часть кода идентична. ООП предлагает изящное решение этой проблемы — метод ParseLine всего-навсего объявляется виртуальным:

type

TTextReader = class

...

function ParseLine(const Line: string): Integer; virtual; //Виртуальный метод

...

end;

Объявление виртуального метода в базовом классе выполняется с помощью ключевого слова virtual, а его перекрытие в производных классах — с помощью ключевого слова override. Перекрытый метод должен иметь точно такой же формат (список параметров, а для функций еще и тип возвращаемого значения), что и перекрываемый:

type

TDelimitedReader = class(TTextReader)

...

function ParseLine(const Line: string): Integer; override;

...

end;
TFixedReader = class(TTextReader)

...

function ParseLine(const Line: string): Integer; override;

...

end;

Суть виртуальных методов в том, что они вызываются по фактическому типу экземпляра, а не по формальному типу, записанному в программе. Поэтому после сделанных изменений метод NextLine будет работать так, как ожидает программист:

function TTextReader.NextLine: Boolean;

var

S: string;

N: Integer;

begin

Result := not EndOfFile;

if Result then

begin

Readln(FFile, S);

N := ParseLine(S); // Работает как <фактический класс>.ParseLine(S)

if N <> ItemCount then

SetLength(FItems, N);

end;

end;

Работа виртуальных методов основана на механизме позднего связывания (late binding). В отличие от раннего связывания (early binding), характерного для статических методов, позднее связывание основано на вычислении адреса вызываемого метода при выполнении программы. Адрес метода вычисляется по хранящемуся в каждом объекте описателю класса.

Благодаря механизму наследования и виртуальных методов в среде Delphi реализуется такая концепция ООП как полиморфизм. Полиморфизм существенно облегчает труд программиста, поскольку обеспечивает повторное использование кода уже написанных и отлаженных методов.

3.8.2. Механизм вызова виртуальных методов

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

Все процедурные переменные с адресами виртуальных методов пронумерованы и хранятся в таблице, называемой таблицей виртуальных методов (VMT — от англ. Virtual Method Table). Такая таблица создается одна для каждого класса объектов, и все объекты этого класса хранят на нее ссылку.

Структуру объекта в оперативной памяти поясняет рисунок 3.3:



Рисунок 3.3. Структура объекта TTextReader в оперативной памяти

Вызов виртуального метода осуществляется следующим образом:

  1. Через объектную переменную выполняется обращение к занятому объектом блоку памяти;

  2. Далее из этого блока извлекается адрес таблицы виртуальных методов (он записан в четырех первых байтах);

  3. На основании порядкового номера виртуального метода извлекается адрес соответствующей подпрограммы;

  4. Вызывается код, находящийся по этому адресу.

Покажем, как можно реализовать косвенный вызов виртуального метода ParseLine (он имеет нулевой номер в таблице виртуальных методов) обычными средствами процедурного программирования:

type

TVMT = array[0..9999] of Pointer;

TParseLineFunc = function (Self: TTextReader; const Line: string): Integer;

var

Reader: TTextReader; // объектная переменна

ObjectDataPtr: Pointer; // указатель на занимаемый объектом блок памяти

VMTPtr: ^TVMT; // указатель на таблицу виртуальных методов

MethodPtr: Pointer; // указатель на метод

begin

...

ObjectDataPtr := Pointer(Reader); // 1) обращение к данным объекта

VMTPtr := Pointer(ObjectDataPtr^); // 2) извлечение адреса VMT

MethodPtr := VMTPtr^[0]; // 3) извлечение адреса метода из VMT

TParseLineFunc(MethodPtr)(Reader, S); // 4) вызов метода

...

end.

Поддержка механизма вызова виртуальных методов на уровне языка Delphi избавляет программиста от всей этой сложности.

3.8.3. Абстрактные виртуальные методы

При построении иерархии классов часто возникает ситуация, когда работа виртуального метода в базовом классе не известна и наполняется содержанием только в наследниках. Так случилось, например, с методом ParseLine, тело которого в классе TTextReader объявлено пустым. Конечно, тело метода всегда можно сделать пустым или почти пустым (так мы и поступили), но лучше воспользоваться директивой abstract:

type

TTextReader = class

...

function ParseLine(const Line: string): Integer; virtual; abstract;

...

end;

Директива abstract записывается после слова virtual и исключает необходимость написания кода виртуального метода для данного класса. Такой метод называется абстрактным, т.е. подразумевает логическое действие, а не конкретный способ его реализации. Абстрактные виртуальные методы часто используются при создании классов-полуфабрикатов. Свою реализацию такие методы получают в законченных наследниках.

3.8.4. Динамические методы

Разновидностью виртуальных методов являются так называемые динамические методы. При их объявлении вместо ключевого слова virtual записывается ключевое слово dynamic, например:

type

TTextReader = class

...

function ParseLine(const Line: string): Integer; dynamic; abstract;

...

end;

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

По смыслу динамические и виртуальные методы идентичны. Различие состоит только в механизме их вызова. Методы, объявленные с директивой virtual, вызываются максимально быстро, но платой за это является большой размер системных таблиц, с помощью которых определяются их адреса. Размер этих таблиц начинает сказываться с увеличением числа классов в иерархии. Методы, объявленные с директивой dynamic вызываются несколько дольше, но при этом таблицы с адресами методов имеют более компактный вид, что способствует экономии памяти. Таким образом, программисту предоставляются два способа оптимизации объектов: по скорости работы (virtual) или по объему памяти (dynamic).

3.8.5. Методы обработки сообщений

Специализированной формой динамических методов являются методы обработки сообщений. Они объявляются с помощью ключевого слова message, за которым следует целочисленная константа — номер сообщения. Следующий пример взят из исходных текстов библиотеки VCL:

type

TWidgetControl = class(TControl)

...

procedure CMKeyDown(var Msg: TCMKeyDown); message CM_KEYDOWN;

...

end;

Метод обработки сообщений имеет формат процедуры и содержит единственный var-параметр. При перекрытии такого метода название метода и имя параметра могут быть любыми, важно лишь, чтобы неизменным остался номер сообщения, используемый для вызова метода. Вызов метода выполняется не по имени, как обычно, а с помощью обращения к специальному методу Dispatch, который имеется в каждом классе (метод Dispatch определен в классе TObject).

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

3.9. Классы в программных модулях

Классы очень удобно собирать в модули. При этом их описание помещается в секцию interface, а код методов — в секцию implementation. Создавая модули классов, нужно придерживаться следующих правил:

  • все классы, предназначенные для использования за пределами модуля, следует определять в секции interface;

  • описание классов, предназначенных для употребления внутри модуля, следует располагать в секции implementation;

  • если модуль B использует модуль A, то в модуле B можно определять классы, порожденные от классов модуля A.

Соберем рассмотренные ранее классы TTextReader, TDelimitedReader и TFixedReader в отдельный модуль ReadersUnit:

unit ReadersUnit;
interface
type

TTextReader = class

private

// Поля

FFile: TextFile;

FItems: array of string;

FActive: Boolean;

// Методы

procedure PutItem(Index: Integer; const Item: string);

// Методы чтения и записи свойств

procedure SetActive(const AActive: Boolean);

function GetItemCount: Integer;

function GetEndOfFile: Boolean;

protected

// Методы чтения и записи свойств

function GetItem(Index: Integer): string;

// Абстрактные методы

function ParseLine(const Line: string): Integer; virtual; abstract;

public

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

constructor Create(const FileName: string);

destructor Destroy; override;

// Методы

function NextLine: Boolean;

// Свойства

property Active: Boolean read FActive write SetActive;

property Items[Index: Integer]: string read GetItem; default;

property ItemCount: Integer read GetItemCount;

property EndOfFile: Boolean read GetEndOfFile;

end;
TDelimitedReader = class(TTextReader)

private

// Поля

FDelimiter: Char;

protected

// Методы

function ParseLine(const Line: string): Integer; override;

public

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

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

// Свойства

property Delimiter: Char read FDelimiter;

end;
TFixedReader = class(TTextReader)

private

// Поля

FItemWidths: array of Integer;

protected

// Методы

function ParseLine(const Line: string): Integer; override;

public

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

constructor Create(const FileName: string;

const AItemWidths: array of Integer);

end;
TMyReader = class(TDelimitedReader)

property FirstName: string index 0 read GetItem;

property LastName: string index 1 read GetItem;

property Phone: string index 2 read GetItem;

end;
implementation
{ TTextReader }
constructor TTextReader.Create(const FileName: string);

begin

inherited Create;

AssignFile(FFile, FileName);

FActive := False;

end;
destructor TTextReader.Destroy;

begin

Active := False;

inherited;

end;
function TTextReader.GetEndOfFile: Boolean;

begin

Result := Eof(FFile);

end;
function TTextReader.GetItem(Index: Integer): string;

begin

Result := FItems[Index];

end;
function TTextReader.GetItemCount: Integer;

begin

Result := Length(FItems);

end;
function TTextReader.NextLine: Boolean;

var

S: string;

N: Integer;

begin

Result := not EndOfFile;

if Result then // Если не достигнут конец файла

begin

Readln(FFile, S); // Чтение очередной строки из файла

N := ParseLine(S); // Разбор считанной строки

if N <> ItemCount then

SetLength(FItems, N); // Отсечение массива (если необходимо)

end;

end;
procedure TTextReader.PutItem(Index: Integer; const Item: string);

begin

if Index > High(FItems) then // Если индекс выходит за границы массива,

SetLength(FItems, Index + 1); // то увеличение размера массива

FItems[Index] := Item; // Установка соответствующего элемента

end;
procedure TTextReader.SetActive(const AActive: Boolean);

begin

if Active <> AActive then // Если состояние изменяется

begin

if AActive then

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

else

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

FActive := AActive; // Сохранение состояния в поле

end;

end;
{ TDelimitedReader }
constructor TDelimitedReader.Create(const FileName: string;

const ADelimiter: Char = ';');

begin

inherited Create(FileName);

FDelimiter := ADelimiter;

end;
function TDelimitedReader.ParseLine(const Line: string): Integer;

var

S: string;

P: Integer;

begin

S := Line;

Result := 0;

repeat

P := Pos(Delimiter, S); // Поиск разделителя

if P = 0 then // Если разделитель не найден, то считается, что

P := Length(S) + 1; // разделитель находится за последним символом

PutItem(Result, Copy(S, 1, P - 1)); // Установка элемента

Delete(S, 1, P); // Удаление элемента из строки

Result := Result + 1; // Переход к следующему элементу

until S = ''; // Пока в строке есть символы

end;
{ TFixedReader }
constructor TFixedReader.Create(const FileName: string;

const AItemWidths: array of Integer);

var

I: Integer;

begin

inherited Create(FileName);

// Копирование AItemWidths в FItemWidths

SetLength(FItemWidths, Length(AItemWidths));

for I := 0 to High(AItemWidths) do

FItemWidths[I] := AItemWidths[I];

end;
function TFixedReader.ParseLine(const Line: string): Integer;

var

I, P: Integer;

begin

P := 1;

for I := 0 to High(FItemWidths) do

begin

PutItem(I, Copy(Line, P, FItemWidths[I])); // Установка элемента

P := P + FItemWidths[I]; // Переход к следующему элементу

end;

Result := Length(FItemWidths); // Количество элементов постоянно

end;
end.

Как можно заметить, в описании классов присутствуют новые ключевые слова private, protected и public. С их помощью регулируется видимость частей класса для других модулей и основной программы. Назначение каждого ключевого слова поясняется ниже.

3.10. Разграничение доступа к атрибутам объектов

Программист может разграничить доступ к атрибутам своих объектов для других программистов (и себя самого) с помощью специальных ключевых слов: private, protected, public, published (последнее не используется в модуле ReadersUnit).

  • Private. Все, что объявлено в секции private недоступно за пределами модуля. Секция private позволяет скрыть те поля и методы, которые относятся к так называемым особеностям реализации. Например, в этой секции класса TTextReader объявлены поля FFile, FActive и FItems, а также методы PutItem, SetActive, GetItemCount и GetEndOfFile.

  • Public. Поля, методы и свойства, объявленные в секции public не имеют никаких ограничений на использование, т.е. всегда видны за пределами модуля. Все, что помещается в секцию public, служит для манипуляций с объектами и составляет программный интерфейс класса. Например, в классе TTextReader в эту секцию помещены конструктор Create, метод NextLine, свойства Active, Items, ItemCount.

  • Protected. Поля, методы и свойства, объявленные в секции protected, видны за пределами модуля только потомкам данного класса; остальным частям программы они не видны. Так же как и private, директива protected позволяет скрыть особенности реализации класса, но в отличие от нее разрешает другим программистам порождать новые классы и обращаться к полям, методам и свойствам, которые составляют так называемый интерфейс разработчика. В эту секцию обычно помещаются виртуальные методы. Примером такого метода является ParseLine.

  • Published. Устанавливает правила видимости те же, что и директива public. Особенность состоит в том, что для элементов, помещенных в секцию published, компилятор генерирует информацию о типах этих элементов. Эта информация доступна во время выполнения программы, что позволяет превращать объекты в компоненты визуальной среды разработки. Секцию published разрешено использовать только тогда, когда для самого класса или его предка включена директива компилятора $TYPEINFO.

Перечисленные секции могут чередоваться в объявлении класса в произвольном порядке, однако в пределах секции сначала следует описание полей, а потом методов и свойств. Если в определении класса нет ключевых слов private, protected, public и published, то для обычных классов всем полям, методам и свойствам приписывается атрибут видимости public, а для тех классов, которые порождены от классов библиотеки VCL, — атрибут видимости published.

Внутри модуля никакие ограничения на доступ к атрибутам классов, реализованных в этом же модуле, не действуют. Кстати, это отличается от соглашений, принятых в некоторых других языках программирования, в частности в языке C++.

3.11. Указатели на методы объектов

В языке Delphi существуют процедурные типы данных для методов объектов. Внешне объявление процедурного типа для метода отличается от обычного словосочетанием of object, записанным после прототипа процедуры или функции:

type

TReadLineEvent = procedure (Reader: TTextReader; const Line: string) of object;

Переменная такого типа называется указателем на метод (method pointer). Она занимает в памяти 8 байт и хранит одновременно ссылку на объект и адрес его метода.

type

TTextReader = class

private

FOnReadLine: TReadLineEvent;

...

public

property OnReadLine: TReadLineEvent read FOnReadLine write FOnReadLine;

end;

Методы объектов, объявленные по приведенному выше шаблону, становятся совместимы по типу со свойством OnReadLine.

type

TForm1 = class(TForm)

procedure HandleLine(Reader: TTextReader; const Line: string);

end;
var

Form1: TForm1;

Reader: TTextReader;

Если установить значение свойства OnReadLine:

Reader.OnReadLine := Form1.HandleLine;

и переписать метод NextLine,

function TTextReader.NextLine: Boolean;

var

S: string;

N: Integer;

begin

Result := not EndOfFile;

if Result then // Если строки для считывания еще есть, то

begin

Readln(FFile, S); // Считывание очередной строки

N := ParseLine(S); // Выделение элементов строки (разбор строки)

if N <> ItemCount then

SetLength(FItems, N);

if Assigned(FOnReadLine) then

FOnReadLine(Self, S); // уведомление о чтении очередной строки

end;

end;

то объект Form1 через метод HandleLine получит уведомление об очередной считанной строке. Обратите внимание, что вызов метода через указатель происходит лишь в том случае, если указатель не равен nil. Эта проверка выполняется с помощью стандартной функции Assigned, которая возвращает True, если ее аргумент является связанным указателем.

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

3.12. Метаклассы

3.12.1. Ссылки на классы

Язык Delphi позволяет рассматривать классы объектов как своего рода объекты, которыми можно манипулировать в программе. Такая возможность рождает новое понятие — класс класса; его принято обозначать термином метакласс.

Для поддержки метаклассов введен специальный тип данных — ссылка на класс (class reference). Он описывается с помощью словосочетания class of, например:

type

TTextReaderClass = class of TTextReader;

Переменная типа TTextReaderClass объявляется в программе обычным образом:

var

ClassRef: TTextReaderClass;

Значениями переменной ClassRef могут быть класс TTextReader и все порожденные от него классы. Допустимы следующие операторы:

ClassRef := TTextReader;

ClassRef := TDelimitedReader;

ClassRef := TFixedReader;

По аналогии с тем, как для всех классов существует общий предок TObject, у ссылок на классы существует базовый тип TClass, определенный, как:

type

TClass = class of TObject;

Переменная типа TClass может ссылаться на любой класс.

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

Физический смысл и взаимосвязь таких понятий, как переменная-объект, экземпляр объекта в памяти, переменная-класс и экземпляр класса в памяти поясняет рисунок 3.4.



Рисунок 3.4. Переменная-объект, экземпляр объекта в памяти, переменная-класс и экземпляр класса в памяти

3.12.2. Методы классов

Метаклассы привели к возникновению нового типа методов — методов класса. Метод класса оперирует не экземпляром объекта, а непосредственно классом. Он объявляется как обычный метод, но перед словом procedure или function записывается зарезервированное слово class, например:

type

TTextReader = class

...

class function GetClassName: string;

end;

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

class function TTextReader.GetClassName: string;

begin

Result := ClassName;

end;

Метод ClassName объявлен в классе TObject и возвращает имя класса, к которому применяется. Очевидно, что надуманный метод GetClassName просто дублирует эту функциональность для класса TTextReader и всех его наследников.

Методы класса применимы и к классам, и к объектам. В обоих случаях в параметре Self передается ссылка на класс объекта. Пример:

var

Reader: TTextReader;

S: string;

begin

// Вызов метода с помощью ссылки на класс

S := TTextReader.GetClassName; // S получит значение 'TTextReader'
// Создание объекта класса TDelimitedReader

Reader := TDelimitedReader.Create('MyData.del');
// Вызов метода с помощью ссылки на объект

S := Reader.GetClassName; // S получит значение 'TDelimitedReader'

end.

Методы классов могут быть виртуальными. Например, в классе TObject определен виртуальный метод класса NewInstance. Он служит для распределения памяти под объект и автоматически вызывается конструктором. Его можно перекрыть в своем классе, чтобы обеспечить нестандартный способ выделения памяти для экземпляров. Метод NewInstance должен перекрываться вместе с другим методом FreeInstance, который автоматически вызывается из деструктора и служит для освобождения памяти. Добавим, что размер памяти, требуемый для экземпляра, можно узнать вызовом предопределенного метода класса InstanceSize.

3.12.3. Виртуальные конструкторы

Особыевозможности ссылок на классы проявляется в сочетании с виртуальными конструкторами. Виртуальный конструктор объявляется с ключевым словом virtual. Вызов виртуального конструктора происходит по фактическому значению ссылки на класс, а не по ее формальному типу. Это позволяет создавать объекты, классы которых неизвестны на этапе компиляции. Механизм виртуальных конструкторов применяется в среде Delphi при восстановлении компонентов формы из файла. Восстановление компонента происходит следующим образом. Из файла считывается имя класса. По этому имени отыскивается ссылка на класс (метакласс). У метакласса вызывается виртуальный конструктор, который создает объект нужного класса.

var

P: TComponent;

T: TComponentClass; // TComponentClass = class of TComponent;

...

T := FindClass(ReadStr);

P := T.Create(nil);

...

На этом закончим изучение теории объектно-ориентированного программирования и в качестве практики рассмотрим несколько широко используемых инструментальных классов среды Delphi. Разберитесь с их назначением и работой. Это поможет глубже понять ООП и пригодится на будущее.

3.13. Классы общего назначения

Как показывает практика, в большинстве задач приходится использовать однотипные структуры данных: списки, массивы, множества и т.д. От задачи к задаче изменяются только их элементы, а методы работы сохраняются. Например, для любого списка нужны процедуры вставки и удаления элементов. В связи с этим возникает естественное желание решить задачу "в общем виде", т.е. создать универсальные средства для управления основными структурами данных. Эта идея не нова. Она давно пришла в голову разработчикам инструментальных пакетов, которые быстро наплодили множество вспомогательных библиотек. Эти библиотеки содержали классы объектов для работы со списками, коллекциями (динамические массивы с переменным количеством элементов), словарями (коллекции, индексированные строками) и другими "абстрактными" структурами. Для среды Delphi тоже разработаны аналогичные классы объектов. Их большая часть сосредоточена в модуле Classes. Наиболее нужными для вас являются списки строк (TStrings, TStringList) и потоки (TStream, THandleStream, TFileStream, TMemoryStream и TBlobStream). Рассмотрим кратко их назначение и применение.

3.13.1. Классы для представления списка строк

Для работы со списками строк служат классы TStrings и TStringList. Они используются в библиотеке VCL повсеместно и имеют гораздо большую универсальность, чем та, что можно почерпнуть из их названия. Классы TStrings и TStringList служат для представления не просто списка строк, а списка элементов, каждый из которых представляет собой пару строка-объект. Если со строками не ассоциированы объекты, получается обычный список строк.

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

Свойства класса TStrings описаны ниже.

Count: Integer — число элементов в списке.

Strings[Index: Integer]: string — обеспечивает доступ к массиву строк по индексу. Первая строка имеет индекс, равный 0. Свойство Strings является основным свойством объекта.

Objects[Index: Integer]: TObject — обеспечивает доступ к массиву объектов. Свойства Strings и Objects позволяют использовать объект TStrings как хранилище строк и ассоциированных с ними объектов произвольных классов.

Text: string — позволяет интерпретировать список строк, как одну большую строку, в которой элементы разделены символами #13#10 (возврат каретки и перевод строки).

Наследники класса TStrings иногда используются для хранения строк вида Имя=Значение, в частности, строк INI-файлов (см. гл. 6). Для удобной работы с такими строками в классе TStrings дополнительно имеются следующие свойства.

Names[Index: Integer]: string — обеспечивает доступ к той части строки, в которой содержится имя.

Values[const Name: string]: string — обеспечивает доступ к той части строки, в которой содержится значение. Указывая вместо Name ту часть строки, которая находится слева от знака равенства, вы получаете ту часть, что находится справа.

Управление элементами списка осуществляется с помощью следующих методов:

Add(const S: string): Integer — добавляет новую строку S в список и возвращает ее позицию. Новая строка добавляется в конец списка.

AddObject(const S: string; AObject: TObject): Integer — добавляет в список строку S и ассоциированный с ней объект AObject. Возвращает индекс пары строка-объект.

AddStrings(Strings: TStrings) — добавляет группу строк в существующий список.

Append(const S: string) — делает то же, что и Add, но не возвращает значения.

Clear — удаляет из списка все элементы.

Delete(Index: Integer) — удаляет строку и ассоциированный с ней объект. Метод Delete, также как метод Clear не разрушают объектов, т.е. не вызывают у них деструктор. Об этом вы должны позаботиться сами.

Equals(Strings: TStrings): Boolean — Возвращает True, если список строк в точности равен тому, что передан в параметре Strings.

Exchange(Index1, Index2: Integer) — меняет два элемента местами.

GetText: PChar — возвращает все строки списка в виде одной большой нуль-терминированной строки.

IndexOf(const S: string): Integer — возвращает позицию строки S в списке. Если заданная строка в списке отсутствует, функция возвращает значение -1.

IndexOfName(const Name: string): Integer — возвращает позицию строки, которая имеет вид Имя=Значение и содержит в себе Имя, равное Name.

IndexOfObject(AObject: TObject): Integer — возвращает позицию объекта AObject в массиве Objects. Если заданный объект в списке отсутствует, функция возвращает значение -1.

Insert(Index: Integer; const S: string) — вставляет в список строку S в позицию Index.

InsertObject(Index: Integer; const S: string; AObject: TObject) — вставляет в список строку S и ассоциированный с ней объект AObject в позицию Index.

LoadFromFile(const FileName: string) — загружает строки списка из текстового файла.

LoadFromStream(Stream: TStream) — загружает строки списка из потока данных (см. ниже).

Move(CurIndex, NewIndex: Integer) — изменяет позицию элемента (пары строка-объект) в списке.

SaveToFile(const FileName: string) — сохраняет строки списка в текстовом файле.

SaveToStream(Stream: TStream) — сохраняет строки списка в потоке данных.

SetText(Text: PChar) — загружает строки списка из одной большой нуль-терминированной строки.

Класс TStringList добавляет к TStrings несколько дополнительных свойств и методов, а также два свойства-события для уведомления об изменениях в списке. Они описаны ниже.

Свойства:

Duplicates: TDuplicates — определяет, разрешено ли использовать дублированные строки в списке. Свойство может принимать следующие значения: dupIgnore (дубликаты игнорируются), dupAccept (дубликаты разрешены), dupError (дубликаты запрещены, попытка добавить в список дубликат вызывает ошибку).

Sorted: Boolean — если имеет значение True, то строки автоматически сортируются в алфавитном порядке.

Методы:

Find(const S: string; var Index: Integer): Boolean — выполняет поиск строки S в списке строк. Если строка найдена, Find помещает ее позицию в переменную, переданную в параметре Index, и возвращает True.

Sort — сортирует строки в алфавитном порядке.

События:

OnChange: TNotifyEvent — указывает на обработчик события, который выполнится при изменении содержимого списка. Событие OnChange генерируется после того, как были сделаны изменения.

OnChanging: TNotifyEvent — указывает на обработчик события, который выполнится при изменении содержимого списка. Событие OnChanging генерируется перед тем, как будут сделаны изменения.

Ниже приводится фрагмент программы, демонстрирующий создание списка строк и манипулирование его элементами:

var

Items: TStrings;

I: Integer;

begin

// Создание списка

Items := TStringList.Create;

Items.Add('Туризм');

Items.Add('Наука');

Items.Insert(1, 'Бизнес');

...

// Работа со списком

for I := 0 to Items.Count - 1 do

Items[I] := UpperCase(Items[I]);

...

// Удаление списка

Items.Free;

end;

3.13.2. Классы для представления потока данных

В среде Delphi существует иерархия классов для хранения и последовательного ввода-вывода данных. Классы этой иерархии называются потоками. Потоки лучше всего представлять как файлы. Классы потоков обеспечивают различное физическое представление данных: файл на диске, раздел оперативной памяти, поле в таблице базы данных (таблица 3.1).

Класс

Описание

TStream

Абстрактный поток, от которого наследуются все остальные. Свойства и методы класса TStream образуют базовый интерфейс потоковых объектов.

THandleStream

Поток, который хранит свои данные в файле. Для чтения-записи файла используется дескриптор (handle), поэтому поток называется дескрипторным. Дескриптор — это номер открытого файла в операционной системе. Его возвращают низкоуровневые функции создания и открытия файла.

TFileStream

Поток, который хранит свои данные в файле. Отличается от ThandleStream тем, что сам открывает (создает) файл по имени, переданному в конструктор.

TMemoryStream

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

TResourceStream

Поток, обеспечивающий доступ к ресурсам в Windows-приложении.

TBlobStream

Обеспечивает последовательный доступ к большим полям таблиц в базах данных.

Таблица 3.1. Классы потоков

Потоки широко применяются в библиотеке VCL и наверняка вам понадобятся. Поэтому ниже кратко перечислены их основные общие свойства и методы.

Общие свойства:

Position: Longint — текущая позиция чтения-записи.

Size: Longint — текущий размер потока в байтах.

Общие методы:

CopyFrom(Source: TStream; Count: Longint): Longint — копирует Count байт из потока Source в свой поток.

Read(var Buffer; Count: Longint): Longint — читает Count байт из потока в буфер Buffer, продвигает текущую позицию на Count байт вперед и возвращает число прочитанных байт. Если значение функции меньше значения Count, то в результате чтения был достигнут конец потока.

ReadBuffer(var Buffer; Count: Longint) — читает из потока Count байт в буфер Buffer и продвигает текущую позицию на Count байт вперед. Если выполняется попытка чтения за концом потока, то генерируется ошибка.

Seek(Offset: Longint; Origin: Word): Longint — продвигает текущую позицию в потоке на Offset байт относительно позиции, заданной параметром Origin. Параметр Origin может иметь одно из следующих значений: 0 — смещение задается относительно начала потока; 1 — смещение задается относительно текущей позиции в потоке; 2 — смещение задается относительно конца потока.

Write(const Buffer; Count: Longint): Longint — записывает в поток Count байт из буфера Buffer, продвигает текущую позицию на Count байт вперед и возвращает реально записанное количество байт. Если значение функции отличается от значения Count, то при записи была ошибка.

WriteBuffer(const Buffer; Count: Longint) — записывает в поток Count байт из буфера Buffer и продвигает текущую позицию на Count байт вперед. Если по какой-либо причине невозможно записать все байты буфера, то генерируется ошибка.

Ниже приводится фрагмент программы, демонстрирующий создание файлового потока и запись в него строки:

var

Stream: TStream;

S: AnsiString;

StrLen: Integer;
begin

// Создание файлового потока

Stream := TFileStream.Create('Sample.Dat', fmCreate);

...

// Запись в поток некоторой строки

StrLen := Length(S) * SizeOf(Char);

Stream.Write(StrLen, SizeOf(Integer)); // запись длины строки

Stream.Write(S, StrLen); // запись символов строки

...

// Закрытие потока

Stream.Free;

end;

3.14. Итоги

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

1   ...   9   10   11   12   13   14   15   16   ...   33


написать администратору сайта