Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

DiVM / OOP / 12_116608_1_51491

.pdf
Скачиваний:
18
Добавлен:
11.05.2015
Размер:
6.45 Mб
Скачать

type

ITextReader = interface(IInterface)

...

end;

IExtendedTextReader = interface(ITextReader)

...

end;

TExtendedTextReader = class(TInterfacedObject, IExtendedTextReader)

...

end;

var

Obj: TExtendedTextReader; Intf: ITextReader;

begin

...

Intf := Obj; // Ошибка! Класс TExtendedTextReader не реализует // интерфейс ITextReader.

...

end;

Рисунок 6.4. Класс TExtendedTextReader совместим лишь с интерфейсом

IExtendedTextReader

Для совместимости с базовым интерфейсом нужно реализовать этот интерфейс явно:

type

TExtendedTextReader = class(TInterfacedObject, ITextReader, IExtendedTextReader)

...

end;

Теперь класс TExtendedTextReader совместим и с интерфейсом ITextReader, поэтому следующее присваивание корректно:

Intf := Obj;

Исключением из только что описанного правила является совместимость всех снабженных интерфейсами объектов с интерфейсом IInterface:

var

Obj: TExtendedTextReader; Intf: IInterface;

begin

...

Intf := Obj; // Правильно, IInterface – особый интерфейс.

...

end;

6.13. Получение интерфейса через другой интерфейс

Через интерфейсную переменную у объекта всегда можно запросить интерфейс другого типа. Для этого используется оператор as, например:

191

var

Intf: IInterface; begin

...

with Intf as ITextReader do Active := True;

...

end;

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

В действительности оператор as преобразуется компилятором в вызов метода

QueryInterface:

var

Intf: IInterface; IntfReader: ITextReader;

...

IntfReader := Intf as ITextReader; // Intf.QueryInterface(ITextReader, IntfReader);

Напомним, что метод QueryInterface описан в интерфейсе IInterface и попадает автоматически во все интерфейсы. Стандартная реализация этого метода находится в классе

TInterfacedObject.

6.14. Механизм подсчета ссылок

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

Подсчет ссылок на объект обеспечивают методы _AddRef и _Release интерфейса IInterface. При копировании значения интерфейсной переменной вызывается метод _AddRef, а при уничтожении интерфейсной переменной — метод _Release. Вызовы этих методов генерируются компилятором автоматически:

var

Intf, Copy: IInterface;

begin

 

...

// Copy._Release; Intf._AddRef;

Copy := Intf;

Intf := nil;

// Intf._Release;

end;

// Copy._Release

Стандартная реализация методов _AddRef и _Release находится в классе TInterfacedObject. Она достаточно проста и вы легко разберетесь с ней, читая комментарии в исходном тексте.

192

type

TInterfacedObject = class(TObject, IInterface)

...

FRefCount: Integer; // Счетчик ссылок function _AddRef: Integer; stdcall;

function _Release: Integer; stdcall;

...

end;

function

TInterfacedObject._AddRef: Integer;

 

begin

:= InterlockedIncrement(FRefCount);

// Увеличение счетчика ссылок

Result

end;

 

 

function

TInterfacedObject._Release: Integer;

 

begin

:= InterlockedDecrement(FRefCount);

// Уменьшение счетчика ссылок

Result

if Result = 0 then

// Если ссылок больше нет, то

Destroy;

// уничтожение объекта

end;

 

 

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

Приведенную выше реализацию методов _AddRef и _Release автоматически получают все наследники класса TInterfacedObject, в том числе и классы TTextReader, TDelimitedReader

и TFixedReader. Поэтому неиспользуемые объекты классов TDelimitedReader и TFixedReader тоже автоматически уничтожаются при работе с ними через интерфейсные переменные:

var

Obj: TDelimitedReader;

Intf, Copy: ITextReader; begin

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

Intf := Obj;

// Obj._AddRef -> Obj.FRefCount = 1

 

Copy := Intf;

// Obj._AddRef -> Obj.FRefCount = 2

 

...

// Obj._Release -> Obj.FRefCount = 1

 

Intf := nil;

-> Obj.Destroy

Copy := nil;

// Obj._Release -> Obj.FRefCount = 0

Obj.Free;

// Ошибка! Объект уже уничтожен и переменная Obj указывает в

никуда end;

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

var

Intf: ITextReader; begin

Intf := TDelimitedReader.Create('MyData.del', ';'); // FRefCount = 1

...

Intf := nil; // FRefCount = 0 -> Destroy end;

Если интерфейс является входным параметром подпрограммы, то при вызове подпрограммы создается копия интерфейсной переменной с вызовом метода _AddRef:

193

procedure LoadItems(R: ITextReader); begin

...

end;

var

Reader: ITextReader; begin

...

LoadItems(Reader); // Создается копия переменной Reader и вызывается

Reader._AddRef end;

Копия не создается, если входной параметр описан с ключевым словом const:

procedure LoadItems(const R: ITextReader); begin

...

end;

var

Reader: ITextRedaer; begin

...

LoadItems(Reader); // Копия не создается, метод _AddRef не вызывается end;

Интерфейсная переменная уничтожается при выходе из области действия переменной, а это значит, что у нее автоматически вызывается метод _Release:

var

Intf: ITextRedaer; begin

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

...

end; // Intf._Release

6.15. Представление интерфейса в памяти

Глубокое понимание работы интерфейсов требует знания их технической реализации. Поэтому вам необходимо разобраться в том, как представляется интерфейс в оперативной памяти компьютера, и что стоит за операторами Intf := Obj и Intf.NextLine.

Интерфейс по сути выступает дополнительной таблицей виртуальных методов, ссылка на которую укладывается среди полей объекта (рисунок 6.5). Эта таблица называется таблицей методов интерфейса. В ней хранятся указатели на методы класса, реализующие методы интерфейса.

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

Intf := Obj; // где Intf: ITextReader и Obj: TTextReader

к адресу объекта добавляется смещение до скрытого поля внутри объекта и этот результат заносится в интерфейсную переменную. Чтобы убедиться в сказанном, посмотрите в отладчике значения Pointer(Obj) и Pointer(Intf) сразу после выполнения оператора Intf := Obj. Эти значения будут разными! Причина в том, что объектная ссылка указывает на начало объекта, а интерфейсная ссылка — на скрытое поле внутри объекта.

194

Рисунок 6.5. Представление интерфейса в памяти

Алгоритм вызова метода интерфейса такой же, как алгоритм вызова метода класса. Когда через интерфейсную переменную выполняется вызов метода,

Intf.NextLine;

реализуется следующий алгоритм:

9.Из интерфейсной переменной извлекается адрес (по нему хранится адрес таблицы методов интерфейса);

10.По полученному адресу извлекается адрес таблицы методов интерфейса;

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

12.Вызывается код, находящийся по этому адресу. Этот код является переходником от метода интерфейса к методу объекта. Его задача — восстановить из ссылки на интерфейс значение указателя Self (путем вычитания заранее известного значения) и выполнить прямой переход на код метода класса.

Обычными средствами процедурного программирования этот алгоритм реализуется так:

type

TMethodTable = array[0..9999] of Pointer; TNextLineFunc = function (Self: ITextReader): Boolean;

var

// интерфейсная переменна

Intf: ITextReader;

IntfPtr: Pointer;

// адрес внутри интерфейсной переменной

TablePtr: ^TMethodTable;

// указатель на таблицу методов интерфейса

MethodPtr: Pointer;

// указатель на метод

begin

 

 

...

// 1)

извлечение адреса из интерфейсной

IntfPtr := Pointer(Intf);

переменной

// 2)

извлечение адреса таблицы методов

TablePtr := Pointer(IntfPtr^);

интерфейса

// 3)

извлечение адреса нужного метода из таблицы

MethodPtr := TablePtr^[3];

TNextLineFunc(MethodPtr)(Intf); // 4)

вызов метода через переходник

...

 

 

end.

 

 

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

6.16. Применение интерфейса для доступа к объекту DLL-

195

библиотеки

Если вы поместите свой класс в DLL-библиотеку, то при необходимости использовать его в главной программе столкнетесь с проблемой. Подключение модуля с классом к главной программе приведет к включению в нее кода всех методов класса, т.е. задача выделения класса в DLL-библиотеку не будет решена. Если же не подключить модуль с описанием класса, главная программа вообще не будет знать о существовании класса, и воспользоваться классом будет невозможно. Эта проблема решается с помощью интерфейсов. Покажем это на примере модуля ReadersUnit.

Сначала вынесем описание интерфейса ITextReader в отдельный модуль (например, ReaderIntf), чтобы этот модуль в дальнейшем можно было подключить к главной программе:

unit ReadersIntf;

interface type

ITextReader = interface(IInterface)

...

end;

implementation

end.

Затем удалим описание интерфейса из модуля ReadersUnit, а вместо него подключим модуль ReaderIntf:

unit ReadersUnit;

interface

uses ReaderIntf;

...

Наконец включим скорректированный модуль ReadersUnit в DLL-библиотеку, которую назовем ReadersLib:

library ReadersLib;

uses

SysUtils, Classes, ReadersUnit;

{$R *.res}

begin end.

Вроде бы все готово, и теперь в главной программе достаточно подключить модуль ReaderIntf и работать с объектами через интерфейс ITextReader (рисунок 6.6).

196

Рисунок 6.6. Схема получения программы и DLL-библиотеки

Но постойте! А как в программе создавать объекты классов, находящихся в DLLбиблиотеке? Ведь в интерфейсе нет методов для создания объектов! Для этого определим в DLL-библиотеке специальную функцию и экспортируем ее:

library ReadersLib;

...

function GetDelimitedReader(const FileName: string; const Delimiter: Char = ';'): ITextReader;

begin

Result := TDelimitedReader.Create(FileName, Delimiter); end;

exports GetDelimitedReader;

begin end.

В главной программе импортируйте функцию GetDelimitedReader, чтобы с ее помощью создавать объекты класса TDelimitedReader:

program Example;

uses ReadersIntf;

function GetDelimitedReader(const FileName: string; const Delimiter: Char = ';'): ITextReader;

external 'ReadersLib.dll' name 'GetDelimitedReader';

var

Intf: ITextReader; begin

Intf := GetDelimitedReader;

...

end.

Теперь вы знаете, как разместить объекты в DLL-библиотеке. Смело пользуйтесь динамически загружаемыми библиотеками, не теряя преимуществ ООП.

6.17. Итоги

Вы прочитали и усвоили весь материал всех предыдущих глав? Тогда спешим вас поздравить! Можете смело утверждать, что знаете язык программирования Delphi. Что же дальше? Вас ждет новая высота — среда программирования Delphi. Сейчас вы имеете лишь поверхностное представление о ее возможностях. Настало время подготовить себя к профессиональной работе в среде Delphi.

Глава 7. Проект приложения

Решаемая на компьютере задача реализуется в виде прикладной программы, которую для краткости называют приложением. В основе разработки приложения в среде Delphi лежит проект. Центральной частью проекта является форма, на которую помещаются необходимые для решения конкретной задачи компоненты. В такой последовательности — проект - формы - компоненты — мы и рассмотрим процесс создания приложения в среде Delphi. По ходу изложения материала мы будем часто обращаться к примеру с вычислением идеального веса, который был рассмотрен в первой главе. Если вы его забыли, перечитайте первую главу еще раз.

7.1. Проект

7.1.1. Понятие проекта

197

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

Файлы описания форм — текстовые файлы с расширением DFM, описывающие формы с компонентами. В этих файлах запоминаются начальные значения свойств, установленные вами в окне свойств.

Файлы программных модулей — текстовые файлы с расширением PAS, содержащие исходные программные коды на языке Delphi. В этих файлах вы пишите методы обработки событий, генерируемых формами и компонентами.

Главный файл проекта — текстовый файл с расширением DPR, содержащий главный программный блок. Файл проекта подключает все используемые программные модули и содержит операторы для запуска приложения. Этот файл среда Delphi создает и контролирует сама.

На основании сказанного можно изобразить процесс создания приложения в среде Delphi от постановки задачи до получения готового выполняемого файла (рисунок 7.1):

Рисунок 7.1. Процесс создания приложения в среде Delphi

Давайте рассмотрим назначение и внутреннее устройство файлов проекта. Это поможет вам легче ориентироваться в проекте.

7.1.2. Файлы описания форм

Помните, с чего вы начинали знакомство со средой Delphi? Конечно, с формы. Итак, первая составная часть проекта — это текстовый файл с расширением DFM, описывающий форму. В DFM-файле сохраняются значения свойств формы и ее компонентов, установленные вами в окне свойств во время проектирования приложения. Количество DFM-файлов равно количеству используемых в приложении форм. Например, в нашем примере об идеальном весе используется только одна форма, поэтому и DFM-файл только один — Unit1.DFM.

Если вы желаете взглянуть на содержимое DFM-файла, вызовите у формы контекстное меню щелчком правой кнопки мыши и выберите команду View as Text (рисунок 7.2).

198

Рисунок 7.2. Переход к текстовому представлению формы с помощью команды View as Text контекстного меню

В ответ среда Delphi вместо графического изображения формы покажет следующий текст в редакторе кода:

199

object Form1: TForm1 Left = 250

Top = 150

Width = 400

Height = 303

Caption = 'Weight Calculator' Color = clBtnFace

Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -11

Font.Name = 'MS Sans Serif' Font.Style = [] OldCreateOrder = False PixelsPerInch = 96 TextHeight = 13

object Label1: TLabel Left = 64

Top = 48

Width = 93

Height = 13

Caption = 'Specify your height:' end

object Label2: TLabel Left = 64

Top = 144

Width = 84

Height = 13

Caption = 'Your ideal weight:' end

object Button1: TButton Left = 248

Top = 64

Width = 75

Height = 25

Caption = 'Compute' TabOrder = 0

OnClick = Button1Click end

object Button2: TButton Left = 248

Top = 160

Width = 75

Height = 25 Caption = 'Close' TabOrder = 1

end

object Edit1: TEdit Left = 64

Top = 64 Width = 121 Height = 21 TabOrder = 2

end

object Edit2: TEdit Left = 64

Top = 160 Width = 121 Height = 21 TabOrder = 3

end end

Несмотря на столь длинный текст описания, разобраться в нем совсем не сложно. Здесь на специальном языке задаются исходные значения для свойств формы Form1 и ее компонентов

Button1, Button2, Edit1, Edit2, Label1, Label2. Большего знать не требуется, поскольку вы всегда будете использовать визуальные средства проектирования и работать с графическим представлением формы, а не с текстовым описанием. Раз так, давайте поспешим вернуться к графическому представлению, не внося в текст никаких изменений. Для этого вызовите контекстное меню редактора кода и выберите команду View as Form (рисунок 7.3).

200