Программирование на языке Delphi. Глава 6. Интерфейсы

Источник: RSDN

предыдущая статья серии

При программировании нередко возникает необходимость выполнить обращение к объекту, находящемуся в другом загрузочном модуле, например EXE или DLL. Для решения поставленной задачи компания Microsoft разработала технологию COM (Component Object Model) - компонентную модель объектов. Технология получила такое название благодаря тому, что обеспечивает создание программных компонентов - независимо разрабатываемых и поставляемых двоичных модулей. Поскольку объекты различных программ разрабатываются на различных языках программирования, например Delphi, C++, Visual Basic и др., технология COM стандартизирует формат взаимодействия между объектами на уровне двоичного представления в оперативной памяти. Согласно технологии COM взаимодействие между объектами осуществляется посредством так называемых интерфейсов. Рассмотрим, что же они собой представляют и как с ними работают.

6.1. Понятие интерфейса

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

Интерфейс = Объект - Реализация

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

6.2. Описание интерфейса

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

  • интерфейсы не могут содержать поля;
  • интерфейсы не могут содержать конструкторы и деструкторы;
  • все атрибуты интерфейсов являются общедоступными (public);
  • все методы интерфейсов являются абстрактными (virtual, abstract).

Приведем пример интерфейса и сразу заметим, что интерфейсам принято давать имена, начинающиеся с буквы I (от англ. Interface):

type
  ITextReader = interface 
    // Методы
    function NextLine: Boolean;
    // Свойства
    property Active: Boolean; 
    property ItemCount: Integer; 
    property Items[Index: Integer]: string; 
    property EndOfFile: Boolean; 
  end;

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

Объяснение состоит в следующем. Не определив интерфейс ITextReader, невозможно разместить класс TTextReader в DLL-библиотеке и обеспечить доступ к нему из EXE-программы. Создавая DLL-библиотеку, мы с помощью оператора uses должны включить модуль ReadersUnit в проект библиотеки. Создавая EXE-программу, мы должны включить модуль ReadersUnit и в нее, чтобы воспользоваться описанием класса TTextReader. Но тогда весь программный код класса попадет внутрь EXE-файла, а это именно то, от чего мы хотим избавиться. Решение проблемы обеспечивается введением понятия интерфейса.

Чтобы вам было легче разобраться с интерфейсом ITextReader, мы привели его незаконченный вариант. Компиляция интерфейса в таком виде приведет к ошибкам: для свойств не указаны методы чтения и записи. Полное описание интерфейса выглядит так:

type
  ITextReader = interface
    // Методы
    function NextLine: Boolean;
    procedure SetActive(const Active: Boolean);
    function GetActive: Boolean;
    function GetItemCount: Integer;
    function GetItem(Index: Integer): string;
    function GetEndOfFile: Boolean;
    // Свойства
    property Active: Boolean read GetActive write SetActive; 
    property Items[Index: Integer]: string read GetItem; default;
    property ItemCount: Integer read GetItemCount; 
    property EndOfFile: Boolean read GetEndOfFile;
  end;

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

6.3. Расширение интерфейса

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

type
  IExtendedTextReader = interface(ITextReader)
    procedure SkipLines(Count: Integer);
  end;

Определенный таким образом интерфейс включает все методы и свойства своего предшественника и добавляет к ним свои собственные. Несмотря на синтаксическое сходство с наследованием классов, расширение интерфейсов имеет другой смысл. В классах наследуется реализация, а в интерфейсах просто расширяется набор методов и свойств.

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

type
  ITextReader = interface
    ...
  end;

эквивалентно следующему:

type
  ITextReader = interface(IInterface)
    ...
  end;

Мы рекомендуем использовать вторую, более полную форму записи.

Описание интерфейса IInterface находится в стандартном модуле System:

type
  IInterface = interface
    ['{00000000-0000-0000-C000-000000000046}']
    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
  end;

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

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

6.4. Глобально-уникальный идентификатор интерфейса

Интерфейс является особым типом данных: он может быть реализован в одной программе, а использоваться из другой. Для этого нужно обеспечить идентификацию интерфейса при межпрограммном взаимодействии. Понятно, что программный идентификатор интерфейса для этого не подходит - разные программы пишутся разными людьми, а разные люди подчас дают одинаковые имена своим творениям. Поэтому каждому интерфейсу выдается своеобразный «паспорт» - глобально-уникальный идентификатор (Globally Unique Identifier - GUID).

Глобально-уникальный идентификатор - это 16-ти байтовое число, представленное в виде заключенной в фигурные скобки последовательности шестнадцатеричных цифр:

{DC601962-28E5-4BF7-9583-0CE22B605045}

В среде Delphi глобально-уникальный идентификатор описывается типом данных TGUID:

type
  PGUID = ^TGUID;
  TGUID = packed record
    D1: Longword;
    D2: Word;
    D3: Word;
    D4: array[0..7] of Byte;
  end;

Константы с типом TGUID разрешено инициализировать строковым представлением глобально-уникального идентификатора. Компилятор сам преобразует строку в запись с типом TGUID. Пример:

const
  InterfaceID: TGUID = '{DC601962-28E5-4BF7-9583-0CE22B605045}';

Если глобально-уникальный идентификатор назначается интерфейсу, то он записывается после ключевого слова interface и заключается в квадратные скобки, например:

type
  IInterface = interface
    ['{00000000-0000-0000-C000-000000000046}']
    ...
  end;

В будущем нашему интерфейсу ITextReader понадобится глобально-уникальный идентификатор. Но как его выбрать так, чтобы он оказался уникальным? Очень просто - нажмите в редакторе кода комбинацию клавиш Ctrl+Shift+G.

type
  ITextReader = interface
    ['{DC601962-28E5-4BF7-9583-0CE22B605045}'] // Результат нажатия Ctrl+Shift+G
    ...
  end;

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

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

Если у интерфейса есть глобально-уникальный идентификатор, то программный идентификатор интерфейса можно использовать там, где ожидается тип данных TGUID, например:

const 
  IID_ITextReader: TGUID = '{DC601962-28E5-4BF7-9583-0CE22B605045}';

function TestInterface(const IID: TGUID): Boolean;

begin 
  ...
  TestInterface(ITextReader);
  // эквивалентно
  TestInterface(IID_ITextReader);
  ...
end;

6.5. Реализация интерфейса

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

type
  TTextReader = class(TObject, ITextReader)
    ...
  end;

Такая запись означает, что класс TTextReader унаследован от класса TObject и реализует интерфейс ITextReader (см. рисунок 6.1).


Рисунок 6.1. Класс TTextReader унаследован от класса TObject и реализует интерфейс ITextReader. Сплошными линиями отмечено наследование классов, а пунктирной линией - реализация интерфейса классом.

Класс, реализующий интерфейс, должен содержать код для всех методов интерфейса. Класс TTextReader в модуле ReadersUnit (см. главу 3) вроде бы содержит код для всех методов интерфейса ITextReader, и все, что нужно сделать, - это добавить имя интерфейса в заголовок класса. Сделайте это в модуле ReadersUnit:

unit ReadersUnit;

interface

type
  ITextReader = interface
    ... 
  end;

  TTextReader = class(TObject, ITextReader) 
    ...
  end;

Если класс содержит только часть методов интерфейса, то недостающие методы придется добавить. Так в интерфейсе ITextReader описан метод GetActive, а в классе TTextReader такого метода нет. Добавьте метод GetActive в класс TTextReader:

type
  TTextReader = class(TObject, ITextReader)
    ...
    function GetActive: Boolean;
    ...
  end;

function TTextReader.GetActive: Boolean;
begin
  Result := FActive;
end;

Но это еще не все. Мы совсем забыли о методах QueryInterface, _AddRef и _Release, которые тоже должны быть реализованы. К счастью, вам нет необходимости ломать голову над реализацией этих методов, поскольку разработчики системы Delphi уже позаботились об этом. Стандартная реализация методов интерфейса IInterface находится в классе TInterfacedObject. Мы его рассмотрим ниже, а сейчас просто унаследуем класс TTextReader от класса TInterfacedObject - и он получит готовую реализацию методов QueryInterface, _AddRef и _Release.

type
  TTextReader = class(TInterfacedObject, ITextReader)
    ...
  end;

Теперь реализация интерфейса ITextReader полностью завершена и можно переходить к использованию объектов класса TTextReader через этот интерфейс.

6.6. Использование интерфейса

Для доступа к объекту через интерфейс нужна интерфейсная переменная:

var
  Intf: ITextReader;

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

Перед использованием интерфейсную переменную инициализируют значением объектной переменной:

var
  Obj: TTextReader;  // объектная переменная
  Intf: ITextReader; // интерфейсная переменная
begin
  ...
  Intf := Obj;
  ...
end;

После инициализации интерфейсную переменную Intf можно использовать для вызова методов объекта Obj:

Intf.Active := True; // -> Obj.SetActive(True);
Intf.NextLine;       // -> Obj.NextLine;

Через интерфейсную переменную доступны только те методы и свойства объекта, которые есть в интерфейсе:

Intf.Free; // Ошибка! У интерфейса ITextReadaer нет метода Free.
Obj.Free;  // Метод Free можно вызвать только так.

6.7. Реализация нескольких интерфейсов

Один класс может содержать реализацию нескольких интерфейсов. Такая возможность позволяет воплотить в классе несколько понятий. Например, класс TTextReader - "считыватель табличных данных" - может выступить еще в одной роли - "считыватель строк". Для этого он должен реализовать интерфейс IStringIterator:

type
  IStringIterator = interface
    function Next: string;
    function Finished: Boolean;
  end;

Интерфейс IStringIterator предназначен для последовательного доступа к списку строк. Метод Next возвращает очередную строку из списка, метод Finished проверяет, достигнут ли конец списка.

Реализуем интерфейс IStringIterator в классе TTextReader таким образом, чтобы последовательно считывались значения из ячеек таблицы. Например, представьте, что в некотором файле дана таблица:

Aaa Bbb Ccc
Ddd Eee Fff
Ggg Hhh Iii

Чтение этой таблицы через интерфейс IStringIterator вернет следующую последовательность строк:

Aaa
Bbb
Ccc
Ddd
Eee
Fff
Ggg
Hhh
Iii

Ниже приведен программный код, обеспечивающий поддержку интерфейса IStringIterator в классе TTextReader:

type
  TTextReader = class(TInterfacedObject, ITextReader, IStringIterator)
    FColumnIndex: Integer;
    function Next: string;
    function Finished: Boolean;
    ...
  end;
...
 
function TTextReader.Next: string;
begin
  if FColumnIndex = ItemCount then // Если пройден последний элемент текущей строки,
  begin                            // то переходим к следующей строке таблицы
    NextLine;
    FColumnIndex := 0;
  end;
  Result := Items[FColumnIndex]; 
  FColumnIndex := FColumnIndex + 1; 
end;

function TTextReader.Finished: string;
begin
  Result := EndOfFile and (FColumnIndex = ItemCount);
end;

Теперь объекты класса TTextReader совместимы сразу с тремя типами данных: TInterfacedObject, ITextReader, IStringIterator.

var
  Obj: TTextReader;
  Reader: ITextReader;
  Iterator: IStringIterator;
begin
  ...
  Reader := Obj;   // Правильно
  Iterator := Obj; // Правильно
  ...
end;

В одном случае объект класса TTextReader рассматривается как считыватель табличных данных, а в другом случае - как обычный список строк с последовательным доступом. Например, если есть две процедуры:

procedure LoadTable(Reader: ITextReader); 
procedure LoadStrings(Iterator: IStringIterator);

то объект класса TTextReader можно передать в обе процедуры:

LoadTable(Obj);   // Obj воспринимается как ITextReader
LoadStrings(Obj); // Obj воспринимается как IStringIterator

6.8. Реализация интерфейса несколькими классами

Несколько совершенно разных классов могут содержать реализацию одного и того же интерфейса. С объектами таких классов можно работать так, будто у них есть общий базовый класс. Интерфейс выступает аналогом общего базового класса.

Рассмотрим пример. Представьте, что есть два класса: TTextReader и TIteratableStringList:

type
  TTextReader = class(TInterfacedObject, ITextReader, IStringIterator)
    ...
  end;

  TIteratableStringList = class(TStringList, IStringIterator)
    ...
  end;

Схематично полученную иерархию классов можно представить так (рисунок 6.2):


Рисунок 6.2. Иерархия классов, реализующих интерфейсы. Сплошными линиями отмечено наследование классов, а пунктирными линиями - реализация интерфейсов классами.

Объекты классов TTextReader и TIteratableStringList несовместимы между собой. Тем не менее, они совместимы с переменными типа IStringIterator. Это значит, что если есть процедура:

procedure LoadStrings(Iterator: IStringIterator);

то вы можете передавать ей объекты обоих упомянутых классов в качестве аргумента:

var
  ReaderObj: TTextReader;
  StringsObj: TIteratableStringList;
begin
  ...
  LoadStrings(ReaderObj);  // Все правильно
  LoadStrings(StringsObj); // Все правильно
  ...
end;

6.9. Связывание методов интерфейса с методами класса

Метод интерфейса связывается с методом класса по имени. Если имена по каким-то причинам не совпадают, то можно связать методы явно с помощью специальной конструкции языка Delphi.

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

type
  TTextReader = class(TInterfacedObject, ITextReader, IStringIterator)
    ...
    function NextItem: string; 
    function IStringIterator.Next := NextItem; // Явное связывание
  end;

При работе с объектами класса TTextReader через интерфейс IStringIterator вызов метода Next приводит к вызову метода NextItem:

var
  Obj: TTextReader;
  Intf: IStringIterator;
begin
  ...
  Intf := Obj;
  Intf.Next; // -> Obj.NextItem;
  ...
end;

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

6.10. Реализация интерфейса вложенным объектом

Случается, что реализация интерфейса содержится во вложенном объекте класса. Тогда не требуется программировать реализацию интерфейса путем замыкания каждого метода интерфеса на соответствующий метод вложенного объекта. Достаточно делегировать реализацию интерфейса вложенному объекту с помощью директивы implements:

type
  TTextParser = class(TInterfacedObject, ITextReader)
    ...
    FTextReader: ITextReader;
    property TextReader: ITextReader read FTextReader implements ITextReader;
    ...
  end;

В этом примере интерфейс ITextReader в классе TTextParser реализуется не самим классом, а его внутренней переменной FTextReader.

Очевидно, что внутренний объект должен быть совместим с реализуемым интерфейсом.

6.11. Совместимость интерфейсов

Совместимость интерфейсов подчиняется определенным правилам. Если интерфейс создан расширением уже существующего интерфейса:

type
  IExtendedTextReader = interface(ITextReader)
    ...
  end;

то интерфейсной переменной базового типа может быть присвоено значение интерфейсной переменной производного типа:

var
  Reader: ITextReader;
  ExtReader: IExtendedTextReader;
begin
  ...
  Reader := ExtReader; // Правильно
  ...
end;

Но не наоборот:

ExtReader := Reader; // Ошибка!

Правило совместимости интерфейсов чаще всего применяется при передаче параметров в процедуры и функции. Например, если процедура работает с переменными типа ITextReader,

procedure LoadFrom(const R: ITextReader); 

то ей можно передать переменную типа IExtendedTextReader:

LoadFrom(ExtReader);

Заметим, что любая интерфейсная переменная совместима с типом данных IInterface - прародителем всех интерфейсов.

6.12. Совместимость класса и интерфейса

Интерфейсной переменной можно присвоить значение объектной переменной при условии, что объект (точнее его класс) реализует упомянутый интерфейс:

var
  Intf: ITextReader; // интерфейсная переменная
  Obj: TTextReader;  // объектная переменная
begin
  ...
  Intf := Obj; // В переменную Intf копируется ссылка на объект Obj
  ...
end;

Такая совместимость сохраняется в производных классах. Если класс реализует некоторый интерфейс, то и все его производные классы совместимы с этим интерфейсом (см. рисунок 6.3):

type
  TTextReader = class(TInterfacedObject, ITextReader)
    ...
  end;

  TDelimitedReader = class(TTextReader)
    ...
  end;

var
  Intf: ITextReader;        // интерфейсная переменная
  Obj: TDelimitedReader;    // объектная переменная
begin
  ...
  Intf := Obj;
  ...
end;


Рисунок 6.3. Классы TTextReader, TDelimitedReader и TFixedReader совместимы с интерфейсом ITextReader

Однако, если класс реализует производный интерфейс, то это совсем не означает, что он совместим с базовым интерфейсом (см. рисунок 6.4):

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, например:

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 := Intf; // Copy._Release; Intf._AddRef;
  Intf := nil;  // Intf._Release; 
end;            // Copy._Release

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

type
  TInterfacedObject = class(TObject, IInterface)
    ...
    FRefCount: Integer;                       // Счетчик ссылок
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
    ...
  end;

function TInterfacedObject._AddRef: Integer;
begin
  Result := InterlockedIncrement(FRefCount);  // Увеличение счетчика ссылок
end;

function TInterfacedObject._Release: Integer;
begin
  Result := InterlockedDecrement(FRefCount);  // Уменьшение счетчика ссылок
  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
  ...
  Intf := nil;   // Obj._Release -> Obj.FRefCount = 1
  Copy := nil;   // Obj._Release -> Obj.FRefCount = 0 -> Obj.Destroy
  Obj.Free;      // Ошибка! Объект уже уничтожен и переменная Obj указывает в никуда
end;

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

var
  Intf: ITextReader;
begin
  Intf := TDelimitedReader.Create('MyData.del', ';'); // FRefCount = 1
  ...
  Intf := nil;                                        // FRefCount = 0 -> Destroy
end;

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

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. Эти значения будут разными! Причина в том, что объектная ссылка указывает на начало объекта, а интерфейсная ссылка - на скрытое поле внутри объекта.


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

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

Intf.NextLine;

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

  1. Из интерфейсной переменной извлекается адрес (по нему хранится адрес таблицы методов интерфейса);
  2. По полученному адресу извлекается адрес таблицы методов интерфейса;
  3. На основании порядкового номера метода в интерфейсе из таблицы извлекается адрес соответствующей подпрограммы;
  4. Вызывается код, находящийся по этому адресу. Этот код является переходником от метода интерфейса к методу объекта. Его задача - восстановить из ссылки на интерфейс значение указателя Self (путем вычитания заранее известного значения) и выполнить прямой переход на код метода класса.

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

type
  TMethodTable = array[0..9999] of Pointer;
  TNextLineFunc = function (Self: ITextReader): Boolean;
var
  Intf: ITextReader;              // интерфейсная переменна
  IntfPtr: Pointer;               // адрес внутри интерфейсной переменной
  TablePtr: ^TMethodTable;        // указатель на таблицу методов интерфейса
  MethodPtr: Pointer;             // указатель на метод
begin
  ...
  IntfPtr := Pointer(Intf);       // 1) извлечение адреса из интерфейсной переменной
  TablePtr := Pointer(IntfPtr^);  // 2) извлечение адреса таблицы методов интерфейса
  MethodPtr := TablePtr^[3];      // 3) извлечение адреса нужного метода из таблицы
  TNextLineFunc(MethodPtr)(Intf); // 4) вызов метода через переходник
  ...
end.

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

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

Если вы поместите свой класс в 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).


Рисунок 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.

следующая статья серии


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=5859