Глава 9
КЛАССЫ И ИНТЕРФЕЙСЫ
Классами в Object Pascal называются специальные типы, которые содержат поля, методы и свойства. Как и любой другой тип, класс служит лишь образцом для создания конкретных экземпляров реализации, которые называются объектами. Сразу же уточню, что в предшественнике Object Pascal - Turbo Pascal объектами называются типы, имеющие много общего с классами Object Pascal. Однако существенные усовершенствования, внесенные в объектную модель Object Pascal, заставили разработчиков языка ввести для обозначения объектов специальный термин - класс, заимствованный, кстати, из Си++. Для совместимости с ранее разработанными программами системы Turbo Pascal with Objects 7.0 в Object Pascal сохранен тип-объект object, поддерживающий “старую” объектную модель. Поскольку все возможности этой модели доступны классам, я не буду рассматривать ее в этой книге, а “освободившийся” термин объект буду употреблять исключительно для обозначения конкретного экземпляра реализации класса.
Важным отличием классов от других типов является то, что объекты класса всегда распределяются в куче, поэтому объект-переменная фактически представляет собой лишь указатель на динамическую область памяти. Однако в отличие от других указателей при ссылке на содержимое объекта запрещается использовать символ “л” за именем объекта:
type
TMyClass = class(TObject) Field: Integer;
end;
var
MyClass: TMyClass;
begin
MyClass^.Field := 0; // Ошибка! Следует писать так:
MyClass.Field := 0;
end;
9.1. ОСНОВНЫЕ ПОНЯТИЯ
Классы - это особое “изобретение” программистов для упрощения разработки сложных программ и улучшения их качества. В основе классов лежат три фундаментальных принципа, которые называются инкапсуляция, наследование и полиморфизм.
9.1.1. Инкапсуляция
Класс представляет собой единство трех сущностей - полей, методов и свойств. Объединение этих сущностей в единое целое и называется инкапсуляцией. Инкапсуляция позволяет во многом изолировать класс от остальных частей программы, сделать его “самодостаточным” для решения конкретной задачи. В результате класс всегда несет в себе некоторую функциональность. Например, класс т Form содержит (инкапсулирует в себе) все необходимое для создания Windows-окна, класс тмето представляет собой полнофункциональный текстовый редактор, класс TTimer обеспечивает работу программы с таймером и т. д.
Инкапсуляция представляет собой мощное средство обмена готовыми к работе программными заготовками. Библиотека классов Delphi - это фактически набор “кирпичиков”, созданных программистами Borland для построения ваших программ.
9.1.2. Наследование
Любой класс может быть порожден от другого класса. Для этого при его объявлении указывается имя класса-родителя:
TChildClass = class (TParentClass)
Порожденный класс автоматически наследует поля, методы и свойства своего родителя и может дополнять их новыми. Таким образом, принцип наследования обеспечивает поэтапное создание сложных классов и разработку собственных библиотек классов.
Все классы Object Pascal порождены от единственного родителя класса TObject. Этот класс не имеет полей и свойств, но включает в себя методы самого общего назначения, обеспечивающие весь жизненный цикл любых объектов - от их создания до уничтожения. Программист не может создать класс, который не был бы дочерним классом TObject. Следующие два объявления идентичны:
TaClass = class(TObject) TaClass = class
Принцип наследования приводит к созданию ветвящегося дерева классов, постепенно разрастающегося при перемещении от TObject к его потомкам. Каждый потомок дополняет возможности своего родителя новыми и передает их своим потомкам.
Для примера на рис. 9.1 показан небольшой фрагмент дерева классов Delphi. Класс Tpersistent обогащает возможности своего родителя TObject: он “умеет” сохранять данные в файле и получать их из него, в результате это умеют делать и все его потомки. Класс TComponent, в свою очередь, умеет взаимодействовать со средой разработчика и передает это умение своим потомкам. Tcontrol не только способен работать с файлами и средой разработчика, но он еще умеет создавать и обслуживать видимые на экране изображения, а его потомок TWinControi может создавать Windows-окна и т. д.
Рис. 9.1. Фрагмент дерева классов Object Pascal
9.1.3. Полиморфизм
Полиморфизм - это свойство классов решать схожие по смыслу проблемы разными способами. В рамках Object Pascal поведенческие свойства класса определяются набором входящих в него методов. Изменяя алгоритм того или иного метода в потомках класса, программист может придавать этим потомкам отсутствующие у родителя специфические свойства. Для изменения метода необходимо перекрыть его в потомке, т. е. объявить в потомке одноименный метод и реализовать в нем нужные действия. В результате в объекте-родителе и объекте-потомке будут действовать два одноименных метода, имеющих разную алгоритмическую основу и, следовательно, придающих объектам разные свойства. Это и называется полиморфизмом объектов.
В Object Pascal полиморфизм достигается не только описанным выше механизмом наследования и перекрытия методов родителя, но и их виртуализацией (см. ниже), позволяющей родительским методам обращаться к методам своих потомков.
9.2. СОСТАВЛЯЮЩИЕ КЛАССА
9.2.1. Поля
Полями называются инкапсулированные в классе данные. Поля могут быть любого типа, в том числе - классами, например:
type TMyClass = class
aIntField: Integer;
aStrField: String;
aObjField: TObject;
end;
Каждый объект получает уникальный набор полей, но общий для всех объектов данного класса набор методов и свойств. Фундаментальный принцип инкапсуляции требует обращаться к полям только с помощью методов и свойств класса. Однако в Object Pascal разрешается обращаться к полям и напрямую:
type
TMyClass = class
FIntField: Integer;
FStrField: String; end;
var
aObject: TMyClass;
begin
aObject.FIntField := 0;
aObject.FStrField := 'Строка символов';
end;
Класс-потомок получает все поля всех своих предков и может дополнять их своими, но он не может переопределять их или удалять.
Таким образом, чем ниже в дереве иерархии располагается класс, тем больше данных получают в свое распоряжение его объекты.
9.2.2. Методы
Инкапсулированные в классе процедуры и функции называются методами. Они объявляются так же, как и обычные подпрограммы:
type
TMyClass = class
Function MyFunc(aPar: Integer): Integer;
Procedure MyProc;
end;
Доступ к методам класса, как и к его полям, возможен с помощью составных имен:
var
aObject: TMyClass;
begin
aObject.MyProc;
end;
Как уже говорилось, методы класса могут перекрываться в потомках. Например:
type
TParentClass = class Procedure DoWork;
end;
TChildClass = class(TParentClass) Procedure DoWork;
end;
Потомки обоих классов могут выполнять сходную по названию процедуру DoWork, но, в общем случае, будут это делать по-разному. Такое замещение методов называется статическим, т. к. реализуется компилятором.
В Object Pascal гораздо чаще используется динамическое замещение методов на этапе прогона программы. Для реализации этого метод, замещаемый в родительском классе, должен объявляться как динамический (с директивой dynamic) или виртуальный (virtual). Встретив такое объявление, компилятор создаст две таблицы -DMT (Dynamic Method Table) и VMT (Virtual Method Table) и поместит в них адреса точек входа соответственно динамических и виртуальных методов. При каждом обращении к замещаемому методу компилятор вставляет код, позволяющий извлечь адрес точки входа в подпрограмму из той или иной таблицы. В классе-потомке замещающий метод объявляется с директивой override (перекрыть). Получив это указание, компилятор создаст код, который на этапе прогона программы поместит в родительскую таблицу точку входа метода класса-потомка, что позволит родителю выполнить нужное действие с помощью нового метода.
Пусть, например, родительский класс с помощью методов show и Hide соответственно показывает что-то на экране или прячет изображение. Для создания изображения он использует метод Draw с логическим параметром:
type
TVisualObject = class(TWinControl)
Procedure Hide;
Procedure Show;
Procedure Draw(IsShow: Boolean); virtual;
end;
TVisualChildObject = class(TVisualObject)
Procedure Draw(IsShow: Boolean); override;
end;
Реализация методов show и Hide очень проста:
Procedure TVisualObject.Show;
begin
Draw(True) ;
end;
Procedure TVisualObject.Hide;
begin
Draw(False) ;
end;
Методы Draw у родителя и потомка имеют разную реализацию и создают разные изображения. В результате родительские методы show и Hide - прятать или показывать те или иные изображения будут в зависимости от конкретной реализации метода Draw у-любого из своих потомков. Динамическое связывание в полной мере реализует полиморфизм классов.
Разница между динамическими и виртуальными методами состоит в том, что таблица динамических методов DMT содержит адреса только -тех методов, которые объявлены как dynamic в данном классе, в то время как таблица VMT содержит адреса виртуальных методов не только данного класса, но и всех его родителей. Значительно большая по размеру таблица VMT обеспечивает более быстрый поиск, в то время как при обращении к динамическому методу программа сначала просматривает таблицу DMT у объекта, затем -у его родительского класса и так далее, пока не будет найдена нужная точка входа.
Динамически перекрываемые методы часто могут вообще ничего не делать. Такие методы называются абстрактными, они обязаны перекрываться в потомках. Программист может запретить вызов абстрактного метода, объявив его с директивой abstract. Например:
type
TVisualObject = class(TWinControl)
Procedure Draw(IsShow: Boolean); virtual; abstract;
end;
TVisualChildObject = class(TWinControl)
Procedure Draw(IsShow: Boolean); override; end;
var
aVisualObject: TVisualObject;
aVisualChild: TVisualChildObject ;
begin
aVisualObject.Show; {Ошибка/ Обращение к абстрактному
методу}
aVisualChild.Show;
{Нормальное обращение. Метод Draw у класса
TVisualChildObject перекрыт.)
end;
Обращение к неперекрытому абстрактному методу вызывает ошибку периода исполнения. Разумеется, в грамотно составленной программе абстрактные методы никогда не вызываются. Классы, содержащие абстрактные методы, называются абстрактными. Такие классы инкапсулируют общие свойства своих неабстрактных потомков, но объекты абстрактных классов никогда не создаются и не используются. Для эксплуатации абстрактных классов в библиотеку классов Delphi включаются классы-потомки, в которых перекрываются абстрактные методы родителя.
В состав любого класса входят два специальных метода -конструктор и деструктор. У класса TObject эти методы называются create и Destroy, так же они называются в подавляющем большинстве его потомков. Конструктор распределяет объект в динамической памяти и помещает адрес этой памяти в переменную self, которая автоматически объявляется в классе. Деструктор удаляет объект из кучи. Обращение к конструктору должно предварять любое обращение к полям и некоторым методам объекта. По своей форме конструкторы и деструкторы являются процедурами, но объявляются с помощью зарезервированных слов constructor и Destructor:
type
TMyClass = class IntField: Integer; Constructor Create(Value: Integer);
Destructor Destroy;
end;
Любые поля объекта, а также методы класса, оперирующие с его полями, могут вызываться только после создания объекта с помощью вызова конструктора, т. к. конструкторы распределяют объект в динамической памяти и делают действительным содержащийся в объекте указатель.
var
MyObject: TMyClass;
begin
MyObject.IntField := 0;
{ Ошибка! Объект не созданконструктором!}
MyObject := TMyClass.Create;
// Надо так: создаем объект
MyObject.IntField := 0;
// и обращаемся к его полю
MyObect.Free;
// Уничтожаем ненужный объект
end;
В базовом классе TObject определен метод Free, который сначала проверяет действительность адреса объекта и лишь затем вызывает деструктор Destroy. Обращение к деструктору объекта будет ошибочным, если объект не создан конструктором, поэтому для уничтожения ненужного объекта следует вызывать метод Free, как это сделано в предыдущем примере.
Большинство конструкторов реализуют некоторые действия, необходимые для правильной работы объекта. Поэтому в конструкторе класса-потомка следует сначала вызвать конструктор своего родителя, а уже затем осуществлять дополнительные действия. Вызов любого метода родительского класса достигается с помощью зарезервированного слова inherited (унаследованный):
Constructor TMyClass.Create(Value: Integer);
// Возможная реализация конструктора
begin
Inherited Create; // Вызываем унаследованный конструктор IntField := Value; // Реализуем дополнительные действия
end;
Некоторые методы могут вызываться без создания и инициации объекта. Такие методы называются методами класса, они объявляются с помощью зарезервированного слова class:
type
TMyClass = class(TObject)
class Function GetClassName: String;
end;
var
S: String;
begin
S := TMyClass.GetClassName;
end;
Методы класса не должны обращаться к полям, т. к. в общем случае вызываются без создания объекта, а следовательно, в момент вызова полей просто не существует. Обычно они возвращают служебную информацию о классе - имя класса, имя его родительского класса, адрес метода и т. п.
9.2.3. Одноименные методы
В отличие от остальных версий Delphi в версиях 4...6 появилась возможность в рамках одного класса иметь несколько одноименных методов. Описанный выше механизм перекрытия родительского метода одноименным методом потомка приводит к тому, что потомок “не видит” перекрытый родительский метод и может обращаться к нему лишь с помощью зарезервированного слова inherited. В Delphi 4 введено зарезервированное слово overload (перезагрузить), с помощью которого становятся видны одноименные методы как родителя, так и потомка.
При обнаружении одноименного метода компилятор Delphi предупреждает о том, что у класса уже есть аналогичный метод с дру
гими параметрами. Для подавления сообщений объявление одноименного метода можно сопровождать зарезервированным словом reintrpduce (вновь ввести).
Чтобы одноименные методы можно было отличить друг от друга,каждый из них должен иметь уникальный набор, параметров. В ходе шлпоянения программы при: обращении к одному, из одноименнх
методов программа проверяет; тип и количество фактических параметров обращения и выбирает нужный метод
В следующем примере в классе TForm1 используются целых 4 одноименных метода close. Лишь один из них - унаследованный метод без параметра выполняет свои основные функции - закрывает окно. Три других отличаются набором параметров и выводят сообщение в заголовок окна.
Поместите на пустую форму четыре кнопки TButton и напишите такие обработчики их событий OnClick:
procedure TForm1.ButtonlClick(Sender: TObject);
begin
Close('Строка символов')
end;
procedure TFormi.Button2Click(Sender: TObject);
begin
Close(123)
end;
procedure TFormi.ButtonSClick(Sender: TObject);
begin
Close (20,300) ;
end;
procedure TFormi.Button4Click(Sender: TObject);
begin
Close end;
Теперь в раздел private класса Tform1 вставьте три таких объявления методов close:
private
{ Private declarations }
procedure Close(S: String);
reintroduce;
overload;
procedure Close(I: Integer);
reintroduce;
overload;
procedure Close(I,J: Integer);
reintroduce;
overload;
И, наконец, в разделе implementation поместите описания объявленных методов:
procedure TForm1.Close(S: String) ;
begin
Caption := S end;
procedure TFormI.Close(I: Integer);
begin
Caption := IntToStr(I) end;
procedure TFormI.close(I,J: Integers);
begin
Caption := IntToStr(i*j)
end;
Теперь после запуска программы три первые кнопки будут вызывать методы close класса Tform1 и менять заголовок окна, в то время как кнопка Button4 обратится к методу close родительского класса т Form и закроет окно.
9.2.4. Свойства
Свойства - это специальный механизм классов, регулирующий доступ к полям. Свойства объявляются с помощью зарезервированных СЛОВ property, read И write (слова read И write считаются зарезервированньши только в контексте объявления свойства). Обычно свойство связано с некоторым полем и указывает те методы класса, которые должны использоваться при записи в это поле или при чтении из него. Например:
type
TaClass = class
IntField: Integer; Function GetField: Integer;
Procedure SetField (Value: Integers);
Property IntegerValue: Integer read GetField
write SetField;
end;
В контексте программы свойство ведет себя как обычное поле. Например, мы могли бы написать такие операторы:
var
aClass: TaClass;
Value: Integer;
begin
aClass := TaClass.Create; { Обязательное обращение к
конструктору перед обращением к полю или свойству!} aClass.IntegerValue := 0;
Value := aClass.IntegerValue;
aClass.Destroy; // Удаление ненужного объекта
end;
Более того, возможен и такой оператор присваивания:
aClass.IntField := NewValue;
Разница между этим оператором и оператором
aClass.IntegerValue := NewValue;
заключается в том, что при обращении к свойству автоматически подключается метод setFieid, в котором могут реализовываться специфичные действия. Вспомним использовавшийся нами в учебной программе оператор
IbOutput.Caption := 'Строка';
Свойство Caption компонента Label вызывает метод setText, который не только запоминает строку символов во внутренней переменной, но и осуществляет прорисовку метки с новым текстом.
Если нет необходимости в специальных действиях при чтении или записи свойства, вместо имени соответствующего метода можно указывать имя поля:
type
TaClass = class IntFiled: Integer;
Procedure SetFieid (Value: Integers;
Property IntegerValue:
Integer read IntFiled write SetFieid;
end;
Если необходимо, чтобы свойство было доступно только для чтения или только для записи, следует опустить соответственно часть write или read. Вообще говоря, свойство может и не связываться с
полем. Фактически оно описывает один или два метода, которые осуществляют некоторые действия над данными того же типа, что и свойство.
9.3. ОБЪЯВЛЕНИЕ КЛАССА
Любой вновь создаваемый класс может содержать секции (разделы), определяемые зарезервированными словами published (опубликованные), private (закрытые), protected (защищенные), public (доступные) и automated (автоматизированные). Внутри каждой секции вначале определяются поля, а затем - методы и свойства.
Секции определяют области видимости элементов описания класса. Секция public не накладывает ограничений на область видимости перечисляемых в ней полей, методов и свойств - их можно вызывать в любом другом модуле программы. Секция published также не ограничивает область видимости, однако в ней перечисляются свойства, которые должны быть доступны не только на этапе исполнения, но и на этапе конструирования программы (т. е. в окне Инспектора объектов). Секция published используется только при разработке нестандартных компонентов. Замечу, что среда Delphi помещает описания компонентов, вставленных в форму, в специальную секцию без названия, которая располагается сразу за заголовком класса и продолжается до первой объявленной секции. Эта секция - published. Программисту не следует помещать в нее собственные элементы описания класса или удалять из нее элементы, вставленные средой. Секция private сужает область видимости до минимума: закрытые элементы описания доступны только внутри методов данного класса и подпрограммах, находящихся в том же модуле, где описан класс. Элемент, объявленный в секции private, становится недоступным даже ближайшим потомкам класса, если они размещаются в других модулях. Секция protected доступна только методам самого класса, а также любым его потомкам, независимо от того, находятся ли они в том же модуле или нет. Наконец, секция automated используется только для объявления свойств и методов, которые будут добавлены к так называемому интерфейсу OLE-объектов Автоматизации; область видимости членов этой секции не ограничена.
В Object Pascal разрешается сколько угодно раз объявлять любую секцию, причем порядок следования секций не имеет значения. Любая секция может быть пустой.
Следующий фрагмент кода поясняет области видимости.
Unit Unit1;
Interface
Uses Controls, Forms;
type
TFormI = class(TForm)
Buttoni: TButton; // Эта секция обслуживается Delphi
// Ее элементы доступны всем
// Эта секция доступна в модуле Uniti
private
FIntField: Integers
Procedure
SetValue(Value: Integers);
Function GetValue: Integer;
published
// Эта секция доступна в любом модуле
Property IntField: read GetValue write SetValue;
protected // Эта секция доступна классам-потомкам
Procedure Proc1;
public // Эта секция доступна в любом модуле Procedure Proc2;
end;
var
Formi: TForm1;
Implementation Procedure TFormI.Proc1 ;
Buttoni.Color := clBtnFace;1
// Так можно
FIntField := 0;
// Так можно
IntField := 0;1
// Так можно Proc1;
// Так можно Proc2;1
// Так можно
end;
begin
Form1.Button1.Color := clBtnFace; // Так можно
Form1.FIntField := 0; // Так можно
Form1.IntField := 0; // Так можно
Form1.Proc1; // Так нельзя!
Form1.Proc2; // Так можно
end.
Unit Unit2;
Interface
Uses Controls, Unit1;
type
TForm2 = class(TFormI) Button2: TButton;
Procedure Button2Click(Sender: TObject);
end;
var
Form2: TForm2;
Implementation
Procedure TForm2.Button2Click(Sender: TObject);
begin
Buttoni.Color := clBtnFace; // Так можно
FIn'tField := 0; // Так нельзя!
IntField := 0; // Так можно
Proc1; // Так можно
Proc2; // Так можно
end;
begin
Form1.Buttoni.Color := clBtnFace; // Так можно
Form1.FIntField := 0; // Так нельзя!
Form1.IntField := 0; // Так можно
Form1.Proc1; //Так нельзя!
Form1.Proc2; // Так можно
end.
При объявлении класса-потомка разрешается перемещать элементы класса из одной области видимости в другую. Для предыдущего примера допустимо такое объявление:
type
TForm2 = class(Tform1)
Public
Procedure Proc1;
end;
После этого в модуле unit2 возможно такое обращение:
Form2.Proc1;
После перемещения в секцию private элемент объявления становится невидим потомкам (если потомок, как это обычно бывает, объявляется в другом модуле), и, следовательно, его уже нельзя переместить в другую секцию.
Класс может объявляться только в интерфейсной области модуля или в самом начале области реализации. Нельзя определять классы в разделе описаний подпрограмм.
9.4. ИНТЕРФЕЙСЫ
Интерфейсы играют главную роль в технологиях СОМ (Component Object Model - компонентная модель объектов), CORBA (Common Object Request Broker Architecture - архитектура с
брокером требуемых общих объектов) и связанных с ними технологиях удаленного доступа, т. е. технологиях доступа к объектам, расположенным (и выполняющимся) на другой машине. Их основная задача - описать свойства, методы и события удаленного объекта в терминах машины клиента, т. е. на используемом при разработке клиентского приложения языке программирования. С помощью интерфейсов программа клиента обращается к удаленному объекту так, как если бы он был ее собственным объектом.
Тема интерфейсов достаточно обширна и интересна. В этой главе даются лишь самые общие сведения об интерфейсах. Сведение этой темы в одну главу с классами не случайно, т. к. интерфейс представляет собой пустой класс, т. е. класс, в котором провозглашены, но никак не расшифрованы свойства и методы.
9.4.1. Создание и использование интерфейса
Интерфейсы представляют собой частный случай описания типов. Они объявляются с помощью зарезервированного слова interface. Например:
type
IEdit = interface
procedure Copy; stdcall;
procedure Cut; stdcall;
procedure Paste; stdcall;
function Undo: Boolean; stdcall;
end;
Такое объявление эквивалентно описанию абстрактного класса в том смысле, что провозглашение интерфейса не требует расшифровки объявленных в нем свойств и методов.
В отличие от классов интерфейс не может содержать поля, и, следовательно, объявляемые в нем свойства в разделах read и write могут ссылаться только на методы. Все объявляемые в интерфейсе члены размещаются в единственной секции public. Методы не могут быть абстрактными (abstract), виртуальными (virtual), динамическими (dynamic) или перекрываемыми (override). Интерфейсы не могут иметь конструкторов или деструкторов, т. к. описываемые в них методы реализуются только в рамках поддерживающих их классов, которые называются интерфейсными.
Если какой-либо класс поддерживает интерфейс (т. е. является интерфейсным), имя этого интерфейса указывается при объявлении класса в списке его родителей:
TEditor = class(TInterfacedObject,IEdit)
procedure Copy; stdcall;
procedure Cut; stdcall;
procedure Paste; stdcall;
function Undo: Boolean; stdcall;
end;
В отличие от обычного класса интерфейсный класс может иметь более одного родительского интерфейса:
type
IMylnterface = interface procedure Delete; stdcall;
end;
TMyEditor = class(TInterfacedObiect, lEdit, IMylnterface)
procedure Copy; stdcall;
procedure Cut; stdcall;
procedure Paste; stdcall;
function Undo:, Boolean; stdcall;
procedure Delete; stdcall;
end;
В любом случае в разделе реализации интерфейсного класса необходимо описать соответствующие интерфейсные методы. Если, например, объявлен интерфейс
IPaint = interface
procedure CirclePaint(Canva: TCanvas; X,Y,R: Integer);
procedure RectPaint(Canva: TCanvas; X1,Y1,X2,Y2: Integer);
end;
и использующий его интерфейсный класс
TPainter = class(TInterfacedObject,IPaint)
procedure CirclePaint(Canva: TCanvas; X,Y,R:
Integers);
procedure RectPaint(Canva: TCanvas; X1,Y1,X2,Y2: Integer);
end;
то в разделе implementation следует указать реализацию методов:
procedure TPainter.CirclePaint(Canva: TCanvas;
X,Y,R: Integers; begin
with Canva do
Ellipse(X, Y, X+2*R, Y+2*R) ;
end;
procedure TPainter.RectPaint(Canva: TCanvas;
X1,Y1,X2,Y2: Integer);
begin
with Canva do
Rectangle(XI, Yl, X2, Y2)
end;
Теперь можно объявить интерфейсный, объект класса TPainter, чтобы с его помощью нарисовать окружность и квадрат:
procedure TFormI.PaintBoxIPaint(Sender: TObject);
var
Painter: IPaint;
begin
Painter := TPainter.Create;
Painter.CirclePaint(PaintBoxl.Canvas,10,0,10) ;
Painter.RectPaint(PaintBoxl.Canvas,40,0,60,20);
end;
Несмотря на то что интерфейс всегда объявляется до объявления использующего его интерфейсного класса и, следовательно, известен компилятору, его методы обязательно должны быть перечислены в объявлении класса. В нашем случае простое указание
type
TPainter = class(TInterfacedObject, IPaint)
end;
было бы ошибкой: компилятор потребовал бы вставить описание методов CirclePaint и RectPaint.
Подобно тому как все классы в Object Pascal порождены от единственного родителя TObject, все интерфейсные классы порождены от общего предка TInterfacedObject. Этот предок умеет распределять память для интерфейсных объектов и использует глобальный интерфейс lunknow:
type
TInterfacedObject = class(TObject, lUnknown) private
FRefCount: Integer;
protected
function Querylnterface(
const IID: TGUID; out Obj): Integer; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
public
property RefCount: Integer read FRefCount;
end;
Если бы в предыдущем примере класс TPainter был описан так:
TPainter = class(IPaint)
procedure CirclePaint(Canva: TCanvas; X,Y,R: Integer);
procedure RectPaint(Canva: TCanvas; X1,Y1,X2,Y2: Integer);
end;
компилятор потребовал бы описать недостающие методы Queryinterface, _Add И _Release класса TInterfacedObject. Поле FRef Count этого класса служит счетчиком вызовов интерфейсного объекта и используется по принятой в Windows схеме: при каждом обращении к методу Add интерфейса IUnknow счетчик наращивается на единицу, при каждом обращении к Release - на единицу сбрасывается. Когда значение этого поля становится равно 0, интерфейсный объект уничтожается и освобождается занимаемая им память.
Если интерфейс предполагаетсяиспользовать в технологиях COM/DCOM или CORBA, его методы должны описывать с директивой stdcall или (для объектов Автоматизации) safecall
К интерфейсному объекту можно применить оператор приведения типов as, чтобы использовать нужный интерфейс:
procedure PaintObjects(P: TInterfacedObject) var
X: IPaint;
begin
try
X := P as IPaint;
X.CirclePaint(PaintBoxl.Canvas,0,0,20)
except
ShowMessage('Объект не поддерживает интерфейс IPaint')
end
end;
Встретив такое присваивание, компилятор создаст код, с помощью которого вызывается метод Queryinterface интерфейса IUnknow с требованием вернуть ссылку на интерфейс IPaint. Если объект не поддерживает указанный интерфейс, возникает исключительная ситуация.
Интерфейсы, рассчитанные на использование в удаленных объектах, должны снабжаться глобально-уникальным идентификатором (guiD). Например:
IPaint = interface
['{A4AFEB60-7705-11D2-8B41-444553540000}']
procedure CirclePaint(Canva: TCanvas; X,Y,R: Integer);
procedure RectPaint(Canva: TCanvas; Xl,Yl,X2,Y2: Integer);
end;
Глобально-уникальные идентификаторы создаются по специальной технологии, гарантирующей ничтожно малую вероятность того, что два guid совпадут. Эта технология включена в Windows 32: чтобы получить guid для вновь созданного интерфейса в среде Delphi, достаточно нажать клавиши Ctrl+Shift+G. Для работы с guid в модуле System объявлены следующие типы:
type
PGUID = ^TGUID;
TGUID = record Dl: LongWord;
D2: Word;
D3: Word;
D4: array [0..7] of Byte;
end;
Программист может объявлять типизированные константы типа tguid, например:
const IID_IPaint: TGUID= ['{A4AFEB61-7705-11D2-8B41-444553540000}'] ;
Константы guid могут использоваться вместо имен интерфейсов при вызове подпрограмм. Например, два следующих обращения идентичны:
procedure Paint(const IID: TGUID);
Paint(IPaint) ;
Paint(IID_Paint);
С помощью зарезервированного слова implements программист может делегировать какому-либо свойству некоторого класса полномочия интерфейса. Это свойство должно иметь тип интерфейса или класса. Если свойство имеет тип интерфейса, имя этого интерфейса должно указываться в списке родителей класса, как если бы это был интерфейсный класс:
type
IMylnterface = interface procedure P1; procedure P2 ;
end;
TMyClass = class(TObject, IMylnterface)
FMyInterface: IMylnterface;
property Mylnterface: IMylnterface
read FMyInterface implements IMylnterface;
end;
Обратите внимание: в этом примере класс TMyciass не является интерфейсным, т. е. классом, в котором исполняются методы p1 и P2. Однако если из него убрать определение уполномоченного свойства Mylnterface, он станет интерфейсным, и в нем должны быть описаны методы интерфейса IMylnterface.
Уполномоченное свойство обязательно должно иметь часть read. Если оно имеет тип класса, класс, в котором оно объявлено, не может иметь других уполномоченных свойств.
9.4.2. Объекты Автоматизации и интерфейс IDispatch
В технологии OLE активно используются так называемые объекты Автоматизации (Automation objects). Эти объекты представляют собой экземпляры интерфейсных классов, родительским интерфейсом которых является специальный интерфейс IDispatch. Отличительной особенностью IDispatch является то обстоятельство, что методы объекта Автоматизации никогда не вызываются напрямую, но всегда - с помощью метода invoke интерфейса IDispatch. Управление объектами СОМ с помощью выполнения методов IDispatch называется маршализацией (marshaling).
Для объявления класса Автоматизации используется специальное зарезервированное слово dispinterface, а перечисляемые в нем методы и свойства должны снабжаться целочисленными идентификаторами, которые вставляются в конце описания методов (свойств) после зарезервированных слов dispid:
type
IStringsDisp = dispinterface ['{EE05DFE2-5549-11DO-9EA9-0020AF3D82DA}']
property ControlDefault[Index: Integer]: OleVariant
dispid 0; default-function Count: Integer;
dispid 1;
property I tern[Index: Integer]: OleVariant dispid 2;
procedure Remove(Index: Integer); dispid 3;
procedure Clear; dispid 4;
function Add(Item: OleVariant): Integer; dispid 5;
function _NewEnum: lUnknown; dispid -4;
end;
В отличие от обычного интерфейсного класса класс Автоматизации не может иметь родительского класса, и поэтому за словом dispinterface нельзя указать список родителей. Идентификаторы методов (свойств) должны быть уникальными в пределах объявления класса. Все возвращаемые функциями и свойствами результаты, а также все параметры обращения к методам должны иметь один из
Следующих типов: Byte, Currency, Real, Double, Longint, Integer, Single, Smallint, AnsiString, WideString, TDateTime, Variant, OleVariant, WordBool или любой интерфейсный тип. За исключением директивы default, которую можно указать для свойства-массива, никакие другие директивы доступа в объявлении методов и свойств не допускаются.
Для доступа к объектам Автоматизации используются переменные типа вариант (см. следующую главу). Инициация такой переменной осуществляется вызовом функции CreateOleObject, определенной в модуле comobj. Эта функция возвращает ссылку на интерфейс IDispatch, с помощью которой можно обращаться к методам и свойствам класса Автоматизации так, как если бы они были методами и свойствами варианта. Например, следующая программа вызывает текстовый процессор MS Word, вставляет в пустую страницу две строки и сохраняет полученный документ на диске:
Uses ComObj ;
var
Word: Variant;
begin
Word := CreateoieObject('Word.Basic');
Word.FileNew('Normal');
Word.Insert('Первая строка'#13);
Word.Insert('Вторая строка'#13);
Word.FileSaveAs('с:\temp\test.txf, 3) ;
end;
Параметром обращения к CreateoieObject является имя сервера Автоматизации, которое должно быть предварительно зарегистрировано в реестре Windows 32. Характерно, что методы сервера не известны на этапе компиляции программы, поэтому компилятор никак не контролирует правильность их вызовов. Названия методов не подчиняются правилам построения идентификаторов Delphi, и в них могут использоваться символы национальных алфавитов.