Interface — Ключевое слово Delphi


Interface — Ключевое слово Delphi

Автор: Анатолий Тенцер

COM (Component Object Model) — модель объектных компонентов — одна из основных технологий, на которых основывается Windows. Более того, все новые технологии в Windows (Shell, Scripting, поддержка HTML и т.п.) реализуют свои API именно в виде COM-интерфейсов. Таким образом, в настоящее время профессиональное программирование требует понимания модели COM и умения с ней работать. В этой главе мы рассмотрим основные понятия COM и особенности их поддержки в Delphi.

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

Интерфейс, образно говоря, является «контрактом» между программистом и компилятором.

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

Компилятор обязуется создать в программе внутренние структуры, позволяющие обращаться к методам этого интерфейса из любого поддерживающего те же соглашения средства программирования. Таким образом, COM является языково-независимой технологией и может использоваться в качестве «клея», соединяющего программы, написанные на разных языках.

Объявление интерфейса включает в себя описание методов и их параметров, но не включает их реализации. Кроме того, в объявлении может указываться идентификатор интерфейса — уникальное 16-байтовое число, сгенерированное по специальным правилам, гарантирующим его статистическую уникальность (GUID — Global Unique Identifier).

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

Таким образом, необходимо понимать следующее:

  • Интерфейс не является классом. Класс может выступать реализацией интерфейса, но класс содержит код методов на конкретном языке программирования, а интерфейс — нет.
  • Интерфейс строго типизирован. Как клиент, так и реализация интерфейса должны использовать точно те же методы и параметры, что указаны в описании интерфейса.
  • Интерфейс является «неизменным контрактом». Нельзя определять новую версию того же интерфейса с измененным набором методов (или их параметров), но с тем же идентификатором.

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

Реализация интерфейса — это код, который реализует эти методы. При этом, за несколькими исключениями, не накладывается никаких ограничений на то, каким образом будет выглядеть реализация. Физически реализация представляет собой массив указателей на методы, адрес которого и используется в клиенте для доступа к COM-объекту. Любая реализация интерфейса имеет метод QueryInterface, позволяющий запросить ссылку на конкретный интерфейс из числа реализуемых.

Автоматическое управление памятью и подсчет ссылок

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

Для поддержки интерфейсов Delphi расширяет синтаксис языка Pascal дополнительными ключевыми словами. Объявление интерфейса в Delphi реализуется ключевым словом interface:

Для генерации нового значения GUID в IDE Delphi служит сочетание клавиш Ctrl+Shift+G.

Базовым интерфейсом в модели COM является IUnknown. Любой интерфейс наследуется от IUnknown и обязан реализовать объявленные в нем методы. IUnknown объявлен в модуле System.pas следующим образом:

Рассмотрим назначение методов IUnknown более подробно.

Последние два метода предназначены для реализации механизма подсчета ссылок.

Эта функция должна увеличить счетчик ссылок на интерфейс на единицу и вернуть новое значение счетчика.

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

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

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

  1. возвращает ссылку на него в параметре Obj;
  2. вызывает метод _AddRef полученного интерфейса;
  3. возвращает 0.

В противном случае — функция возвращает код ошибки E_NOINTERFACE.

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

В модуле System.pas объявлен класс TInterfacedObject, реализующий IUnknown и его методы. Рекомендуется использовать этот класс для создания реализаций своих интерфейсов.

Кроме того, поддержка интерфейсов реализована в базовом классе TObject. Он имеет метод

Если класс реализует запрошенный интерфейс, то функция:

  1. возвращает ссылку на него в параметре Obj;
  2. вызывает метод _AddRef полученного интерфейса;
  3. возвращает TRUE.

В противном случае — функция возвращает FALSE.

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

Реализацией интерфейса в Delphi всегда выступает класс. Для этого в объявлении класса необходимо указать, какие интерфейсы он реализует.

Класс TMyClass реализует интерфейсы IMyInterface и IDropTarget. Необходимо понимать, что реализация классом нескольких интерфейсов не означает множественного наследования и вообще наследования класса от интерфейса. Указание интерфейсов в описании класса означает только то, что в данном классе реализованы все эти интерфейсы.

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

Рассмотрим более подробный пример.

Здесь класс TTest реализует интерфейс ITest. Рассмотрим использование интерфейса из программы.

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

Во-первых, оператор присваивания при приведении типа данных к интерфейсу неявно вызывает метод _AddRef. При этом количество ссылок на интерфейс увеличивается на единицу.

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

Внимание! Если у класса запрошен хотя бы один интерфейс — не вызывайте его метод Free (или Destroy). Класс будет освобожден тогда, когда отпадет необходимость в последней ссылке на его интерфейсы. Если вы к этому моменту уничтожили экземпляр класса вручную — произойдет ошибка доступа к памяти.

Так, следующий код приведет к ошибке в момент выхода из функции:

Если вы хотите уничтожить реализацию интерфейса немедленно, не дожидаясь выхода переменной за область видимости, – просто присвойте ей значение NIL:

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

  1. При приведении типа объекта к интерфейсу вызывается метод _AddRef.
  2. При выходе переменной, ссылающейся на интерфейс, за область видимости либо при присвоении ей другого значения вызывается метод _Release.
  3. Единожды запросив у объекта интерфейс, в дальнейшем вы не должны освобождать объект вручную. Вообще начиная с этого момента лучше работать с объектом только через интерфейсные ссылки.

В рассмотренных примерах код для получения интерфейса у класса генерировался (с проверкой типов) на этапе компиляции. Если класс не реализует требуемого интерфейса, то программа не откомпилируется. Однако существует возможность запросить интерфейс и во время выполнения программы. Для этого служит оператор as, который вызывает QueryInterface и, в случае успеха, возвращает ссылку на полученный интерфейс. В противном случае генерируется исключение.

Например, следующий код будет успешно откомпилирован, но при выполнении вызовет ошибку «Interface not supported»:

будет успешно компилироваться и выполняться.

Реализация интерфейсов (расширенное рассмотрение)

Рассмотрим вопросы реализации интерфейсов подробнее.

Объявим два интерфейса:

Теперь создадим класс, который будет реализовывать оба этих интерфейса:

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

Если реализация методов TTest2.Beep1 и TTest2.Beep2 идентична, то можно не создавать два разных метода, а объявить класс следующим образом:

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

Для делегирования реализации интерфейса другому классу служит ключевое слово implements.

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

Обращаться к полученному классу можно точно так же, как и к любому классу, реализующему интерфейсы:

Интерфейсы и TComponent

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

Этот код проверяет наличие у всех форм в приложении возможности реализации интерфейса IGetData и в случае, если форма реализует этот интерфейс, вызывает его метод.

Использование интерфейсов внутри программы

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

В качестве примера рассмотрим MDI-приложение, имеющее много различных форм и единую панель инструментов. Предположим, что на этой панели инструментов имеются команды «Сохранить», «Загрузить» и «Очистить», однако каждое из окон реагирует на эти команды по-разному.

Создадим модуль с объявлениями интерфейсов:

Интерфейс IToolBarCommands описывает набор методов, которые должны реализовать формы, поддерживающие работу с панелью кнопок. Метод SupportedCommands возвращает список поддерживаемых формой команд.

Создадим три дочерние формы — Form2, Form3 и Form4 — и установим им свойство FormStyle = fsMDIChild.

Form2 умеет выполнять все три команды:

Form3 умеет выполнять только команду Clear:

И наконец, Form4 вообще не реализует интерфейс IToolBarCommands и не откликается ни на одну команду.

На главной форме приложения поместим ActionList и создадим три компонента TAction. Кроме того, разместим на ней TToolBar и назначим ее кнопкам соответствующие TAction.

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

При активизации команд проверяется наличие активной дочерней формы, у нее запрашивается интерфейс IToolBarCommands и вызывается соответствующий ему метод:

Работа программы представлена на рисунке.

Того же эффекта можно добиться и другими методами (например, унаследовав все дочерние формы от единого предка либо обмениваясь с ними сообщениями), однако эти методы имеют ряд существенных недостатков.

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

Использование интерфейсов для реализации Plug-In

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

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

Объявим интерфейсы модуля расширения и внутреннего API программы.

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

Plug-In представляет собой DLL, экспортирующую функцию CreateFilter, возвращающую ссылку на интерфейс ILoadFilter. Главный модуль сначала должен вызвать метод Init, передав в него имя файла и ссылку на интерфейс внутреннего API, а затем вызывать метод GetNextLine до тех пор, пока он не вернет FALSE.

Рассмотрим код модуля расширения:

Метод Init выполняет две задачи: сохраняет ссылку на интерфейс API главного модуля для дальнейшего использования и пытается открыть файл с данными. Если файл открыт успешно – выставляется внутренний флаг InitSuccess.

Метод GetNextLine считывает следующую строку данных и возвращает либо TRUE, если это удалось, либо FALSE — в случае окончания файла. Кроме того, при помощи API, предоставляемого главным модулем, данный метод информирует пользователя о ходе загрузки.

В деструкторе мы обнуляем ссылку на API главного модуля, уничтожая его, и закрываем файл.

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

Теперь полученную DLL можно использовать из основной программы.

Класс TAPI реализует API, предоставляемый модулю расширения. Функция ShowMessage выводит сообщения модуля в Status Bar главной формы приложения.

Подготавливаем TMemo к загрузке данных:

Получаем имя модуля с фильтром для выбранного расширения файла. Описания модулей хранятся в файле plugins.ini в секции Filters в виде строк формата:

Теперь попытаемся загрузить модуль и найти в нем функцию CreateFilter:

Функция найдена, создаем экземпляр фильтра и инициализируем его. Поскольку внутренний API реализован тоже как интерфейс — нет необходимости сохранять ссылку на него.

Загружаем данные при помощи созданного фильтра:

Перед выгрузкой DLL из памяти необходимо обязательно освободить ссылку на интерфейс Plug-In, иначе это произойдет по выходе из функции и вызовет Access Violation.

Выгружаем DLL и обновляем TMemo:

Таким образом, реализовать модули расширения для загрузки данных из форматов, отличающихся от текстового, и единообразно работать с ними несложно.

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

Внимание! Поскольку в EXE и DLL используются длинные строки, не забудьте включить в список uses обоих проектов модуль ShareMem. Другим вариантом решения проблемы передачи строк является использование типа данных WideString. Для них распределением памяти занимается OLE, причем делает это независимо от модуля, из которого была создана строка.

Object Interfaces (Delphi)

An object interface, or simply interface, defines methods that can be implemented by a class. Interfaces are declared as classes, but cannot be directly instantiated and do not have their own method definitions. Rather, it is the responsibility of any class that supports an interface to provide implementations for the method of the interface. A variable of an interface type can reference an object whose class implements that interface; however, only methods declared in the interface can be called using such a variable.

Interfaces offer some of the advantages of multiple inheritance without the semantic difficulties. They are also essential for using distributed object models (such as SOAP). Using a distributed object model, custom objects that support interfaces can interact with objects written in C++, Java, and other languages.

Contents

Interface Types

Interfaces, like classes, can be declared only in the outermost scope of a program or unit, not in a procedure or function declaration. An interface type declaration has the form:

Warning: The ancestorInterface and GUID specification are required to support Win32 COM interoperability. If your interface is to be accessed through COM, be sure to specify the ancestorInterface and GUID.

In most respects, interface declarations resemble class declarations, but the following restrictions apply:

  • The memberList can include only methods and properties. Fields are not allowed in interfaces.
  • Since an interface has no fields, property read and write specifiers must be methods.
  • All members of an interface are public. Visibility specifiers and storage specifiers are not allowed. (But an array property can be declared as default.)
  • Interfaces have no constructors or destructors. They cannot be instantiated, except through classes that implement their methods.
  • Methods cannot be declared as virtual, dynamic, abstract, or override. Since interfaces do not implement their own methods, these designations have no meaning.

Here is an example of an interface declaration:

In some interface declarations, the interface reserved word is replaced by dispinterface.

IInterface and Inheritance

An interface, like a class, inherits all of its ancestors’ methods. But interfaces, unlike classes, do not implement methods. What an interface inherits is the obligation to implement methods, an obligation that is passed onto any class supporting the interface.

The declaration of an interface can specify an ancestor interface. If no ancestor is specified, the interface is a direct descendant of IInterface , which is defined in the System unit and is the ultimate ancestor of all other interfaces. On Win32, IInterface declares three methods: QueryInterface , _AddRef , and _Release .

Note: IInterface is equivalent to IUnknown . You should generally use IInterface for platform independent applications and reserve the use of IUnknown for specific programs that include Win32 dependencies.

QueryInterface provides the means to obtain a reference to the different interfaces that an object supports. _AddRef and _Release provide lifetime memory management for interface references. The easiest way to implement these methods is to derive the implementing class from the TInterfacedObject of the System unit. It is also possible to dispense with any of these methods by implementing it as an empty function; COM objects, however, must be managed through _AddRef and _Release .

Warning: QueryInterface , _AddRef , and _Release are required to support Win32 COM interoperability. If your interface is to be accessed through COM, be sure to implement these methods.

Interface Identification and GUIDs

An interface declaration can specify a globally unique identifier (GUID), represented by a string literal enclosed in brackets immediately preceding the member list. The GUID part of the declaration must have the form:

where each x is a hexadecimal digit (0 through 9 or A through F). The Type Library editor automatically generates GU > Ctrl+Shift+G in the code editor.

A GUID is a 16-byte binary value that uniquely identifies an interface. If an interface has a GUID, you can use interface querying to get references to its implementations.

Note: GUIDs are only used for COM interoperability.

The TGU >System unit, are used to manipulate GUIDs.

Supports can be called in either of two ways:

Note: The SysUtils unit provides an overloaded function called Supports that returns true or false when class types and instances support a particular interface represented by a GUID. The Supports function is used in the manner of the Delphi is and as operators. The significant difference is that the Supports function can take as the right operand either a GUID or an interface type associated with a GUID, whereas is and as take the name of a type. For more information about is and as, see Class References.

Calling Conventions for Interfaces

The default calling convention for interface methods is register, but interfaces shared among modules (especially if they are written in different languages) should declare all methods with stdcall. On Win32, you can use safecall to implement methods of dual interfaces.

Interface Properties

Properties declared in an interface are accessible only through expressions of the interface type; they cannot be accessed through class-type variables. Moreover, interface properties are visible only within programs where the interface is compiled.

In an interface, property read and write specifiers must be methods, since fields are not available.

Forward Declarations

An interface declaration that ends with the reserved word interface and a semicolon, without specifying an ancestor, GUID, or member list, is a forward declaration. A forward declaration must be resolved by a defining declaration of the same interface within the same type declaration section. In other words, between a forward declaration and its defining declaration, nothing can occur except other type declarations.

Forward declarations allow mutually dependent interfaces. For example:

Mutually derived interfaces are not allowed. For example, it is not legal to derive IWindow from IControl and also derive IControl from IWindow .

Delphi и COM

Delphi , Технологии , COM и DCOM

Автор: Анатолий Тенцер

COM (Component Object Model) — модель объектных компонентов — одна из основных технологий, на которых основывается Windows. Более того, все новые технологии в Windows (Shell, Scripting, поддержка HTML и т.п.) реализуют свои API именно в виде COM-интерфейсов. Таким образом, в настоящее время профессиональное программирование требует понимания модели COM и умения с ней работать. В этой главе мы рассмотрим основные понятия COM и особенности их поддержки в Delphi.

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

Интерфейс, образно говоря, является «контрактом» между программистом и компилятором.

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

Компилятор обязуется создать в программе внутренние структуры, позволяющие обращаться к методам этого интерфейса из любого поддерживающего те же соглашения средства программирования. Таким образом, COM является языково-независимой технологией и может использоваться в качестве «клея», соединяющего программы, написанные на разных языках.

Объявление интерфейса включает в себя описание методов и их параметров, но не включает их реализации. Кроме того, в объявлении может указываться идентификатор интерфейса — уникальное 16-байтовое число, сгенерированное по специальным правилам, гарантирующим его статистическую уникальность (GUID — Global Unique Identifier).

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

Таким образом, необходимо понимать следующее:

  • Интерфейс не является классом. Класс может выступать реализацией интерфейса, но класс содержит код методов на конкретном языке программирования, а интерфейс — нет.
  • Интерфейс строго типизирован. Как клиент, так и реализация интерфейса должны использовать точно те же методы и параметры, что указаны в описании интерфейса.
  • Интерфейс является «неизменным контрактом». Нельзя определять новую версию того же интерфейса с измененным набором методов (или их параметров), но с тем же идентификатором.

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

Реализация интерфейса — это код, который реализует эти методы. При этом, за несколькими исключениями, не накладывается никаких ограничений на то, каким образом будет выглядеть реализация. Физически реализация представляет собой массив указателей на методы, адрес которого и используется в клиенте для доступа к COM-объекту. Любая реализация интерфейса имеет метод QueryInterface, позволяющий запросить ссылку на конкретный интерфейс из числа реализуемых.

Автоматическое управление памятью и подсчет ссылок

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

Для поддержки интерфейсов Delphi расширяет синтаксис языка Pascal дополнительными ключевыми словами. Объявление интерфейса в Delphi реализуется ключевым словом interface:

Для генерации нового значения GUID в IDE Delphi служит сочетание клавиш Ctrl+Shift+G.

Базовым интерфейсом в модели COM является IUnknown. Любой интерфейс наследуется от IUnknown и обязан реализовать объявленные в нем методы. IUnknown объявлен в модуле System.pas следующим образом:

Рассмотрим назначение методов IUnknown более подробно.

Последние два метода предназначены для реализации механизма подсчета ссылок.

Эта функция должна увеличить счетчик ссылок на интерфейс на единицу и вернуть новое значение счетчика.

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

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

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

  1. возвращает ссылку на него в параметре Obj;
  2. вызывает метод _AddRef полученного интерфейса;
  3. возвращает 0.

В противном случае — функция возвращает код ошибки E_NOINTERFACE.

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

В модуле System.pas объявлен класс TInterfacedObject, реализующий IUnknown и его методы. Рекомендуется использовать этот класс для создания реализаций своих интерфейсов.

Кроме того, поддержка интерфейсов реализована в базовом классе TObject. Он имеет метод

Если класс реализует запрошенный интерфейс, то функция:

  1. возвращает ссылку на него в параметре Obj;
  2. вызывает метод _AddRef полученного интерфейса;
  3. возвращает TRUE.

В противном случае — функция возвращает FALSE.

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

Реализацией интерфейса в Delphi всегда выступает класс. Для этого в объявлении класса необходимо указать, какие интерфейсы он реализует.

Класс TMyClass реализует интерфейсы IMyInterface и IDropTarget. Необходимо понимать, что реализация классом нескольких интерфейсов не означает множественного наследования и вообще наследования класса от интерфейса. Указание интерфейсов в описании класса означает только то, что в данном классе реализованы все эти интерфейсы.

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

Рассмотрим более подробный пример.

Здесь класс TTest реализует интерфейс ITest. Рассмотрим использование интерфейса из программы.

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

Во-первых, оператор присваивания при приведении типа данных к интерфейсу неявно вызывает метод _AddRef. При этом количество ссылок на интерфейс увеличивается на единицу.

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

Внимание! Если у класса запрошен хотя бы один интерфейс — не вызывайте его метод Free (или Destroy). Класс будет освобожден тогда, когда отпадет необходимость в последней ссылке на его интерфейсы. Если вы к этому моменту уничтожили экземпляр класса вручную — произойдет ошибка доступа к памяти.

Так, следующий код приведет к ошибке в момент выхода из функции:

Если вы хотите уничтожить реализацию интерфейса немедленно, не дожидаясь выхода переменной за область видимости, – просто присвойте ей значение NIL:

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

  1. При приведении типа объекта к интерфейсу вызывается метод _AddRef.
  2. При выходе переменной, ссылающейся на интерфейс, за область видимости либо при присвоении ей другого значения вызывается метод _Release.
  3. Единожды запросив у объекта интерфейс, в дальнейшем вы не должны освобождать объект вручную. Вообще начиная с этого момента лучше работать с объектом только через интерфейсные ссылки.

В рассмотренных примерах код для получения интерфейса у класса генерировался (с проверкой типов) на этапе компиляции. Если класс не реализует требуемого интерфейса, то программа не откомпилируется. Однако существует возможность запросить интерфейс и во время выполнения программы. Для этого служит оператор as, который вызывает QueryInterface и, в случае успеха, возвращает ссылку на полученный интерфейс. В противном случае генерируется исключение.

Например, следующий код будет успешно откомпилирован, но при выполнении вызовет ошибку «Interface not supported»:

будет успешно компилироваться и выполняться.

Реализация интерфейсов (расширенное рассмотрение)

Рассмотрим вопросы реализации интерфейсов подробнее.

Объявим два интерфейса:

Теперь создадим класс, который будет реализовывать оба этих интерфейса:

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

Если реализация методов TTest2.Beep1 и TTest2.Beep2 идентична, то можно не создавать два разных метода, а объявить класс следующим образом:

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

Для делегирования реализации интерфейса другому классу служит ключевое слово implements.

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

Обращаться к полученному классу можно точно так же, как и к любому классу, реализующему интерфейсы:

Интерфейсы и TComponent

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

Этот код проверяет наличие у всех форм в приложении возможности реализации интерфейса IGetData и в случае, если форма реализует этот интерфейс, вызывает его метод.

Использование интерфейсов внутри программы

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

В качестве примера рассмотрим MDI-приложение, имеющее много различных форм и единую панель инструментов. Предположим, что на этой панели инструментов имеются команды «Сохранить», «Загрузить» и «Очистить», однако каждое из окон реагирует на эти команды по-разному.

Создадим модуль с объявлениями интерфейсов:

Интерфейс IToolBarCommands описывает набор методов, которые должны реализовать формы, поддерживающие работу с панелью кнопок. Метод SupportedCommands возвращает список поддерживаемых формой команд.

Создадим три дочерние формы — Form2, Form3 и Form4 — и установим им свойство FormStyle = fsMDIChild.

Form2 умеет выполнять все три команды:

Form3 умеет выполнять только команду Clear:

И наконец, Form4 вообще не реализует интерфейс IToolBarCommands и не откликается ни на одну команду.

На главной форме приложения поместим ActionList и создадим три компонента TAction. Кроме того, разместим на ней TToolBar и назначим ее кнопкам соответствующие TAction.

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

При активизации команд проверяется наличие активной дочерней формы, у нее запрашивается интерфейс IToolBarCommands и вызывается соответствующий ему метод:

Работа программы представлена на рисунке.

Того же эффекта можно добиться и другими методами (например, унаследовав все дочерние формы от единого предка либо обмениваясь с ними сообщениями), однако эти методы имеют ряд существенных недостатков.

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

Использование интерфейсов для реализации Plug-In

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

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

Объявим интерфейсы модуля расширения и внутреннего API программы.

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

Plug-In представляет собой DLL, экспортирующую функцию CreateFilter, возвращающую ссылку на интерфейс ILoadFilter. Главный модуль сначала должен вызвать метод Init, передав в него имя файла и ссылку на интерфейс внутреннего API, а затем вызывать метод GetNextLine до тех пор, пока он не вернет FALSE.

Рассмотрим код модуля расширения:

Метод Init выполняет две задачи: сохраняет ссылку на интерфейс API главного модуля для дальнейшего использования и пытается открыть файл с данными. Если файл открыт успешно – выставляется внутренний флаг InitSuccess.

Метод GetNextLine считывает следующую строку данных и возвращает либо TRUE, если это удалось, либо FALSE — в случае окончания файла. Кроме того, при помощи API, предоставляемого главным модулем, данный метод информирует пользователя о ходе загрузки.

В деструкторе мы обнуляем ссылку на API главного модуля, уничтожая его, и закрываем файл.

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

Теперь полученную DLL можно использовать из основной программы.

Класс TAPI реализует API, предоставляемый модулю расширения. Функция ShowMessage выводит сообщения модуля в Status Bar главной формы приложения.

Подготавливаем TMemo к загрузке данных:

Получаем имя модуля с фильтром для выбранного расширения файла. Описания модулей хранятся в файле plugins.ini в секции Filters в виде строк формата:

Теперь попытаемся загрузить модуль и найти в нем функцию CreateFilter:

Функция найдена, создаем экземпляр фильтра и инициализируем его. Поскольку внутренний API реализован тоже как интерфейс — нет необходимости сохранять ссылку на него.

Загружаем данные при помощи созданного фильтра:

Перед выгрузкой DLL из памяти необходимо обязательно освободить ссылку на интерфейс Plug-In, иначе это произойдет по выходе из функции и вызовет Access Violation.

Выгружаем DLL и обновляем TMemo:

Таким образом, реализовать модули расширения для загрузки данных из форматов, отличающихся от текстового, и единообразно работать с ними несложно.

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

Внимание! Поскольку в EXE и DLL используются длинные строки, не забудьте включить в список uses обоих проектов модуль ShareMem. Другим вариантом решения проблемы передачи строк является использование типа данных WideString. Для них распределением памяти занимается OLE, причем делает это независимо от модуля, из которого была создана строка.

Статья Delphi и COM раздела Технологии COM и DCOM может быть полезна для разработчиков на Delphi и FreePascal.

Комментарии и вопросы

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

Образовательный блог — всё для учебы

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

Объявление интерфейса
Объявление интерфейса может включать указание интерфейса предка. Если никакой предок не определен, то объявляемый интерфейс — прямой потомок от интерфейса IUnknown, определенного в модуле System Delphi и являющегося общим предком всех интерфейсов.

Синтаксис объявление типа интерфейса имеет следующий вид:

Туре
=Interface[( )]
[‘’] // Global Unique IDentifier
// Объявление методов и свойств
End;

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

• Объявление интерфейса может включать только методы и свойства (поля не предусмотрены). Таким образом, интерфейс — это просто описание связанных свойств и методов без реализации. Команды объявления разделов видимости в интерфейсе не разрешены. Все элементы интерфейса объявляются как в разделе Public класса, поэтому они видны всюду, где виден сам интерфейс.

• Интерфейсы не имеют никаких конструкторов или деструкторов. Они не могут быть использованы иначе, чем через классы, которые реализуют их методы.

• Методы не могут быть объявлены с директивами Virtual, Dynamic, Abstract, или Override. Так как интерфейсы не реализуют собственных методов, эти обозначения не имеют никакого значения.

• Все объявляемые в интерфейсе методы являются виртуальными и абстрактными. Таким образом, фактически Interface объявляет описание виртуальной таблицы методов, аналогичной VMT.


• Так как интерфейс не может иметь полей, команды свойств Read и Write должны обращаться только к методам. Команды хранения свойств не разрешены, но свойство-массив может быть объявлено как Default.

• Объявление интерфейса обычно включает глобальный уникальный идентификатор GUID, представляемый строковым литералом, заключенным в скобки, и предшествующий списку определений интерфейса. Для интерфейсов его называют IID (Interface IDentifier). Объявление GUID должно иметь вид:

Где каждый х — шестнадцатеричная цифра (0 до 9 или от А до F).
GUID — 16-байтовое двоичное значение, которое уникально идентифицирует интерфейс, компонентный класс или объект.
• Могут быть объявлены типизированные константы типа TGUID:

Type IMalloc=Interface(IUnknown)
[‘ <00000002-0000-0000-cooo-000000000046>’]
Function AUoc(Size: Integer): Pointer; stdcall;
Function ReAlloc(P: Pointer; Size: Integer): Pointer; stdcall;
End;

Интерфейс IUnknown объявлен в блоке System Delphi так:

IUnknown=Interface
[‘<00000000-0000-0000-СООО-000000000046>’]
Function QueryInterface(Const IID: TGUID; Out Obj): HResult; stdcall;
Function _AddRef: Integer; stdcall;
Function _Release: Integer; stdcall;
End;

Фактически, для превращения обычного объекта OP в СОМ-объект необходимо реализовать интерфейс IUnknown, определив три стандартных метода СОМ:
• Querylnterface позволяет запрашивать (вызывать) интерфейсы объектов после получения указателя на объект-сервер. Клиент с помощью этого метода передает объекту-серверу в виде параметра IID нужного интерфейса. Если объект-сервер поддерживает этот интерфейс, то он возвращает указатель на него в параметре Obj (иначе возвращается Nil). При этом сама функция при положительном результате возвращает 0 и константу E_NOINTERFACE в противном случае. После получения указателя клиент может вызывать методы этого интерфейса.

Intf:=TNewClass.Create As IMalloc; // Пусть TNewClass реализует INewMalloc
Intf.QueryInterface(INewMalloc, Newlntf);
If Assigned(NewIntf) Then Labell.Caption:=NewIntf.Name;

Можно и другим путем проверить поддержку нужного интерфейса:
• _AddRef и _Release обеспечивают работу механизма учета ссылок интерфейса. Поскольку объект-сервер может быть затребован несколькими клиентами, то с помощью этих методов подсчитывается число подключившихся и отключившихся клиентов. Когда число ссылок (или подключившихся клиентов) становится равным нулю, объект-сервер уничтожает сам себя. Фактически интерфейс IUnknown — это механизм, который использует технология СОМ для подсчета числа ссылок и управления жизнью объекта.

Самый простой способ реализовать эти методы в классе состоит в том, чтобы наследовать реализацию класса TInterfacedObject из модуля System. Он может использоваться в программах, не экспортирующих СОМ-объекты.

Класс TInterfacedObject реализует интерфейс IUnknown. Поэтому TlnterfacedObject объявляет и реализует каждый из трех методов интерфейса IUnknown.

Соглашение о Вызовах:
Используемое по умолчанию в Delphi соглашение о вызовах — это register . Для связывания модулей, особенно, если они написаны на разных языках, необходимо объявлять все методы интерфейсов, используемых в СОМ, с stdcall. Следует использовать safecall, чтобы выполнить методы двойных (Dual) интерфейсов или интерфейсов CORBA.

Возможны директивы: register, pascal, cdecl, stdcall, safecall. Первые две означают передачу параметров слева направо в подпрограммы, а остальные наоборот. Все, кроме cdecl удаляют параметры из стека после возврата. Stdcall и safecall используются при обращении к функциям Windows API.

Предописание
Объявление интерфейса может заканчиваться зарезервированным словом Interface и точкой с запятой, т.е. без указания предка, GUID, или/и списка объявлений интерфейса. Такое объявление называется предописанием и аналогично директиве Forward в Паскале. Однако между предописанием и последующими объявлениями не может быть ничего другого, кроме объявлений типа. Предописания позволяют объявлять взаимно зависимые интерфейсы.

Однако взаимно ссылающиеся друг на друга интерфейсы не допускаются. Например, нельзя получить IWindow из IControI и также получить IControl из IWindow.

Поддержка интерфейсов в Delphi
Интерфейсы в Delphi реализуются через классы. Для поддержки интерфейсов в ряде классов Delphi включены дополнительные методы.
• В классе TObject определены методы:
• Class Function GetlnterfaceTable: PInterfaceTable; — Возвращает указатель на таблицу интерфейсов.

Таким образом, таблица интерфейсов состоит из четырех колонок, наиболее важными из которых являются две первых. Первая колонка каждой строки таблицы содержит идентификатор интерфейса (IID), вторая — указатель на VMT таблицу конкретного интерфейса (VTable).
Указатель на таблицу виртуальных методов можно получить и из VMT таблицы класса. Указатель со смещением — 72 (vmtlntfTable) указывает на начальный адрес этой таблицы. Фактически таблица методов интерфейса располагается вслед за таблицей VMT класса и начинается она с методов Querylnterface, _AddRef и _Release. Далее следуют остальные методы интерфейсов, причем последний примыкает к VMT.

• GetInterface(Const IID: TGUID; Out Obj): Boolean; — Возвращает через параметр Obj указатель на интерфейс, если он поддерживается. Фактически возвращает указатель на VTable таблицу интерфейса.

Метод Getlnterface просматривает первую колонку таблицы интерфейсов, сверяя ее содержимое с заданным IID интерфейса. Если соответствие найдено, то параметр Obj возвращает указатель на VTable таблицу интерфейса. В противном случае он возвращает Nil. Функция возвращает True|False.

• На уровне класса TComponent реализована поддержка интерфейса IUnknown — определены методы _AddRef, _Release и Querylnterface. Таким образом, любой компонент Delphi поддерживает интерфейс IUnknown.

Delphi. Интерфейсы в простых примерах

Решил разобраться для себя с тем, что такое интерфейсы, хотя бы в первом приближении. Много где они используются в современном Delphi – и FireMonkey и в COM и др. В данной статье – все самое простое – что такое интерфейс и как им пользоваться в самом простом случае.

Что такое интерфейс?

Интерфейс, это сущность, содержащая в себе уникальный номер (GUID) и список методов, но не содержащая в себе их реализацию. Для реализации создается отдельный класс, в родителях которого прописывается данный интерфейс. Интерфейс, также, не содержит свойств.

Блог GunSmoker-а (переводы)

. when altering one’s mind becomes as easy as programming a computer, what does it mean to be human.

четверг, 21 июля 2011 г.

Чистые интерфейсы в Delphi

Это перевод Pure interfaces in Delphi. Автор: Hallvard Vassbotn.

В комментарии к последнему посту Huseyn спросил:

В Delphi все интерфейсы являются наследниками IUnknown (или IInterface — Hallvard Vassbotn), но в мире C есть ещё более базовая сущность — интерфейсы, не являющиеся наследниками IUnknown . Я встретил такой интерфейс в одной DLL, которую мне надо было использовать. Но я не могу это сделать в Delphi.

Да, это верно, что в Delphi создание интерфейса всегда подразумевает неявное наследование от IUnknown или IInterface (два имени для отличий COM и Delphi интерфейсов). И это означает, что все интерфейсы в Delphi будут иметь три метода QueryInterface , _AddRef и _Release . Это, конечно, делает сложным (или невозможным) реализацию чистого интерфейса из не-COM DLL с использованием синтаксиса объявления интерфейсов в Delphi.

В далёкие тёмные времена Delphi 2, у нас не было явных интерфейсов, а поддержка COM строилась на том факте, что структура таблицы VMT совпадает с бинарным контрактом COM, а «интерфейсы» были объявлены с использованием полностью абстрактных классов. К примеру, именно так до сих пор интерфейсы объявляются в C++. Класс, который хочет реализовать такой «интерфейс», просто наследуется от чистого абстрактного класса, замещая и реализуя все его абстрактные методы. Это достаточно ограниченный способ написания интерфейсов; поскольку Delphi не поддерживает множественное наследование классов, то вы точно захотите уметь поддерживать множество интерфейсов — и тогда вам придётся писать по одному новому классу на каждый интерфейс и вручную писать метод QueryInterface , чтобы он возвращал корректную ссылку. В C++ нет ограничения на множественное наследование и, быть может, поэтому в C++ нет отдельной языковой конструкции для интерфейсов — по сравнению с языками вроде Java, Delphi и C#, которые имеют модель единственности наследования.

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

К примеру: Этот маленький пример кода сначала объявляет чистый абстрактный класс с виртуальными абстрактными методами. Это объявление не-COM интерфейса в стиле C++ для DLL из вопроса. Мы реализуем «интерфейс» созданием класса-наследника от этого базового полностью абстрактного класса, в котором реализуем все методы. Заметьте, что компилятор не требует повторно указывать соглашение вызова при замещении методов: И, наконец, я написал немного тестового кода, который осуществляет вызов реализованных методов через ссылку на «интерфейс». Это соответствует коду в DLL, для которой мы обеспечиваем реализацию интерфейсов.

3 комментария:

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

Этот маленький пример кода НЕ объявляет чистый абстрактный класс, потому что все классы неявно наследуются от TObject, который имеет неабстрактные виртуальные методы.

Не-COM интерфейс в стиле C++ — это не class, а старый паскалевский object.

Объясните толком про интерфейсы в ООП (Delphi). Как их использовать?

Все статьи, которые я нашел выглядят примерно так:
«Сейчас мы объясним вам простыми словами как считать до десяти, и так начнем:
Один, Два, Три, Четрыевдлырже, фхзгЗрыжпр хнге-034 53 уцзхкщхйц е. Ну вот, теперь вы знаете как считать до десяти.»

Все что я понял, интерфейс это некая названная сущность (как класс), которая имеет «пустые» методы (т.е. описания названий, но не их код).
А вот что дальше с этим знанием делать не понятно. Может какой-то простенький пример, практический, есть?
Например — нам нужно сделать то-то, для этого мы используем интерфейс для того-то и делаем мы это так.

  • Вопрос задан более двух лет назад
  • 589 просмотров

В общем. Штука какая.
Начнем с того, что множественное наследование классов, во многих ЯП, запрещено.
Но иногда очень хочется, правда ведь?
Есть интерфейсы. В них вы описываете, что они будут делать (перечисляете методы). Например, пользователь системы может быть клиентом банка, а может быть внутренним сотрудником. А если он клиент, то он еще может быть юр.лицом (фирма) или физ.лицом (просто Ваня, который оформил кредит в банке).
Так вот. Этот Ваня, может реализовывать интерфейс пользователя системы (логиниться, в личный кабинет смотреть, получать уведомления и другие, общие для всех операции) и может реализовывать интерфейс физлица (смотреть свои счета, выпускать карты, делать вклады, платить за телефон).
Две разные сущности сосредоточены в Ване — неплохо было бы их разграничить

Ну это так, приквел.
Вообще, попробуйте разобраться вот с этой статьей (metanit.com/sharp/tutorial/3.9.php). Она про C#, но идея интерефейса такая же, да и код не должен показаться вам совсем уж непонятным.
Но расписано вполне себе доступно

А как только разберетесь, что такое интерфейс и зачем он, вот вам вопрос, который задают на собеседованиях: в каких случаях использовать интерфейс, а в каких — абстрактный класс?

Интерфейсы в Delphi отвечают за две малосвязанных вещи.
1. Множественное наследование. Об этом уже рассказали до меня, повторяться не буду.
2. Подсчёт ссылок (для этого реализатор должен корректно поддерживать _AddRef и _Release, но это уже другой вопрос, и подходящая реализация есть в TInterfacedObject).
Связано это с тем, что Delphi должен был поддерживать Microsoft COM, а там автоматическое управление через подсчёт ссылок.
Так что интерфейсы часто приплетают только потому, что удобно работать с подсчётом ссылок.

Delphi и COM

Delphi и COM

Введение

COM (Component Object Model) — модель объектных компонентов — одна из основных технологий, на которых основывается Windows. Более того, все новые технологии в Windows (Shell, Scripting, поддержка HTML и т.п.) реализуют свои API именно в виде COM-интерфейсов. Таким образом, в настоящее время профессиональное программирование требует понимания модели COM и умения с ней работать. В этой главе мы рассмотрим основные понятия COM и особенности их поддержки в Delphi.

Базовые понятия

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

Интерфейс

Интерфейс, образно говоря, является «контрактом» между программистом и компилятором.

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

Компилятор обязуется создать в программе внутренние структуры, позволяющие обращаться к методам этого интерфейса из любого поддерживающего те же соглашения средства программирования. Таким образом, COM является языково-независимой технологией и может использоваться в качестве «клея», соединяющего программы, написанные на разных языках.

Объявление интерфейса включает в себя описание методов и их параметров, но не включает их реализации. Кроме того, в объявлении может указываться идентификатор интерфейса — уникальное 16-байтовое число, сгенерированное по специальным правилам, гарантирующим его статистическую уникальность (GUID — Global Unique Identifier).

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

Таким образом, необходимо понимать следующее:

  • Интерфейс не является классом. Класс может выступать реализацией интерфейса, но класс содержит код методов на конкретном языке программирования, а интерфейс — нет.
  • Интерфейс строго типизирован. Как клиент, так и реализация интерфейса должны использовать точно те же методы и параметры, что указаны в описании интерфейса.
  • Интерфейс является «неизменным контрактом». Нельзя определять новую версию того же интерфейса с измененным набором методов (или их параметров), но с тем же идентификатором.

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

Реализация интерфейса — это код, который реализует эти методы. При этом, за несколькими исключениями, не накладывается никаких ограничений на то, каким образом будет выглядеть реализация. Физически реализация представляет собой массив указателей на методы, адрес которого и используется в клиенте для доступа к COM-объекту. Любая реализация интерфейса имеет метод QueryInterface, позволяющий запросить ссылку на конкретный интерфейс из числа реализуемых.

Автоматическое управление памятью и подсчет ссылок

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

Объявление интерфейсов

Для поддержки интерфейсов Delphi расширяет синтаксис языка Pascal дополнительными ключевыми словами. Объявление интерфейса в Delphi реализуется ключевым словом interface:

Для генерации нового значения GUID в IDE Delphi служит сочетание клавиш Ctrl+Shift+G.

IUnknown

Базовым интерфейсом в модели COM является IUnknown. Любой интерфейс наследуется от IUnknown и обязан реализовать объявленные в нем методы. IUnknown объявлен в модуле System.pas следующим образом:

Рассмотрим назначение методов IUnknown более подробно.

Последние два метода предназначены для реализации механизма подсчета ссылок.

Эта функция должна увеличить счетчик ссылок на интерфейс на единицу и вернуть новое значение счетчика.

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

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

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

  1. возвращает ссылку на него в параметре Obj;
  2. вызывает метод _AddRef полученного интерфейса;
  3. возвращает 0.

В противном случае — функция возвращает код ошибки E_NOINTERFACE.

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

В модуле System.pas объявлен класс TInterfacedObject, реализующий IUnknown и его методы. Рекомендуется использовать этот класс для создания реализаций своих интерфейсов.

Кроме того, поддержка интерфейсов реализована в базовом классе TObject. Он имеет метод

Если класс реализует запрошенный интерфейс, то функция:

  1. возвращает ссылку на него в параметре Obj;
  2. вызывает метод _AddRef полученного интерфейса;
  3. возвращает TRUE.

В противном случае — функция возвращает FALSE.

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

Реализация интерфейсов

Реализацией интерфейса в Delphi всегда выступает класс. Для этого в объявлении класса необходимо указать, какие интерфейсы он реализует.

Класс TMyClass реализует интерфейсы IMyInterface и IDropTarget. Необходимо понимать, что реализация классом нескольких интерфейсов не означает множественного наследования и вообще наследования класса от интерфейса. Указание интерфейсов в описании класса означает только то, что в данном классе реализованы все эти интерфейсы.

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

Рассмотрим более подробный пример.

Здесь класс TTest реализует интерфейс ITest. Рассмотрим использование интерфейса из программы.

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

Во-первых, оператор присваивания при приведении типа данных к интерфейсу неявно вызывает метод _AddRef. При этом количество ссылок на интерфейс увеличивается на единицу.

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

Внимание! Если у класса запрошен хотя бы один интерфейс — не вызывайте его метод Free (или Destroy). Класс будет освобожден тогда, когда отпадет необходимость в последней ссылке на его интерфейсы. Если вы к этому моменту уничтожили экземпляр класса вручную — произойдет ошибка доступа к памяти.

Так, следующий код приведет к ошибке в момент выхода из функции:

Если вы хотите уничтожить реализацию интерфейса немедленно, не дожидаясь выхода переменной за область видимости, – просто присвойте ей значение NIL:

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

  1. При приведении типа объекта к интерфейсу вызывается метод _AddRef.
  2. При выходе переменной, ссылающейся на интерфейс, за область видимости либо при присвоении ей другого значения вызывается метод _Release.
  3. Единожды запросив у объекта интерфейс, в дальнейшем вы не должны освобождать объект вручную. Вообще начиная с этого момента лучше работать с объектом только через интерфейсные ссылки.

В рассмотренных примерах код для получения интерфейса у класса генерировался (с проверкой типов) на этапе компиляции. Если класс не реализует требуемого интерфейса, то программа не откомпилируется. Однако существует возможность запросить интерфейс и во время выполнения программы. Для этого служит оператор as, который вызывает QueryInterface и, в случае успеха, возвращает ссылку на полученный интерфейс. В противном случае генерируется исключение.

Например, следующий код будет успешно откомпилирован, но при выполнении вызовет ошибку «Interface not supported»:

В то же время код

будет успешно компилироваться и выполняться.

Реализация интерфейсов (расширенное рассмотрение)

Рассмотрим вопросы реализации интерфейсов подробнее.

Объявим два интерфейса:

Теперь создадим класс, который будет реализовывать оба этих интерфейса:

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

Если реализация методов TTest2.Beep1 и TTest2.Beep2 идентична, то можно не создавать два разных метода, а объявить класс следующим образом:

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

Для делегирования реализации интерфейса другому классу служит ключевое слово implements.

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

Обращаться к полученному классу можно точно так же, как и к любому классу, реализующему интерфейсы:

Интерфейсы и TComponent

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

Этот код проверяет наличие у всех форм в приложении возможности реализации интерфейса IGetData и в случае, если форма реализует этот интерфейс, вызывает его метод.

Использование интерфейсов внутри программы

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

В качестве примера рассмотрим MDI-приложение, имеющее много различных форм и единую панель инструментов. Предположим, что на этой панели инструментов имеются команды «Сохранить», «Загрузить» и «Очистить», однако каждое из окон реагирует на эти команды по-разному.

Создадим модуль с объявлениями интерфейсов:

Интерфейс IToolBarCommands описывает набор методов, которые должны реализовать формы, поддерживающие работу с панелью кнопок. Метод SupportedCommands возвращает список поддерживаемых формой команд.

Создадим три дочерние формы — Form2, Form3 и Form4 — и установим им свойство FormStyle = fsMDIChild.

Form2 умеет выполнять все три команды:

Form3 умеет выполнять только команду Clear:

И наконец, Form4 вообще не реализует интерфейс IToolBarCommands и не откликается ни на одну команду.

На главной форме приложения поместим ActionList и создадим три компонента TAction. Кроме того, разместим на ней TToolBar и назначим ее кнопкам соответствующие TAction.

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

При активизации команд проверяется наличие активной дочерней формы, у нее запрашивается интерфейс IToolBarCommands и вызывается соответствующий ему метод:

Того же эффекта можно добиться и другими методами (например, унаследовав все дочерние формы от единого предка либо обмениваясь с ними сообщениями), однако эти методы имеют ряд существенных недостатков.

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

Использование интерфейсов для реализации Plug-In

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

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

Объявим интерфейсы модуля расширения и внутреннего API программы.

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

Plug-In представляет собой DLL, экспортирующую функцию CreateFilter, возвращающую ссылку на интерфейс ILoadFilter. Главный модуль сначала должен вызвать метод Init, передав в него имя файла и ссылку на интерфейс внутреннего API, а затем вызывать метод GetNextLine до тех пор, пока он не вернет FALSE.

Рассмотрим код модуля расширения:

Метод Init выполняет две задачи: сохраняет ссылку на интерфейс API главного модуля для дальнейшего использования и пытается открыть файл с данными. Если файл открыт успешно – выставляется внутренний флаг InitSuccess.

Метод GetNextLine считывает следующую строку данных и возвращает либо TRUE, если это удалось, либо FALSE — в случае окончания файла. Кроме того, при помощи API, предоставляемого главным модулем, данный метод информирует пользователя о ходе загрузки.

В деструкторе мы обнуляем ссылку на API главного модуля, уничтожая его, и закрываем файл.

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

Теперь полученную DLL можно использовать из основной программы.

Класс TAPI реализует API, предоставляемый модулю расширения. Функция ShowMessage выводит сообщения модуля в Status Bar главной формы приложения.

Подготавливаем TMemo к загрузке данных:

Получаем имя модуля с фильтром для выбранного расширения файла. Описания модулей хранятся в файле plugins.ini в секции Filters в виде строк формата:

Теперь попытаемся загрузить модуль и найти в нем функцию CreateFilter:

Функция найдена, создаем экземпляр фильтра и инициализируем его. Поскольку внутренний API реализован тоже как интерфейс — нет необходимости сохранять ссылку на него.

Загружаем данные при помощи созданного фильтра:

Перед выгрузкой DLL из памяти необходимо обязательно освободить ссылку на интерфейс Plug-In, иначе это произойдет по выходе из функции и вызовет Access Violation.

Выгружаем DLL и обновляем TMemo:

Таким образом, реализовать модули расширения для загрузки данных из форматов, отличающихся от текстового, и единообразно работать с ними несложно.

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

Внимание! Поскольку в EXE и DLL используются длинные строки, не забудьте включить в список uses обоих проектов модуль ShareMem. Другим вариантом решения проблемы передачи строк является использование типа данных WideString. Для них распределением памяти занимается OLE, причем делает это независимо от модуля, из которого была создана строка.

COM-сервер, структура и использование

Модель COM предоставляет возможность создания многократно используемых компонентов, независимых от языка программирования. Такие компоненты называются COM-серверами и представляют собой исполняемые файлы (EXE) или динамические библиотеки (DLL), специальным образом оформленные для обеспечения возможности их универсального вызова из любой программы, написанной на поддерживающем COM языке программирования. При этом COM-сервер может выполняться как в адресном пространстве вызывающей программы (In-Process-сервер), так и в виде самостоятельного процесса (Out-Of-Process-сервер) или даже на другом компьютере (Distributed COM). COM автоматически разрешает вопросы, связанные с передачей параметров (Marshalling) и согласованием потоковых моделей клиента и сервера.

Далее будут рассмотрены некоторые архитектурные вопросы, знание которых необходимо для работы с COM.

COM-сервер

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

Сервер в виде DLL

Такой сервер всегда выполняется в адресном пространстве активизировавшего его приложения (In-Process). За счет этого, как правило, снижаются накладные расходы на вызов методов сервера. В то же время такой сервер менее надежен, поскольку его память не защищена от ошибок в вызывающем приложении. Кроме того, он не может выполняться на удаленной машине без исполнимого модуля-посредника, способного создать процесс, в который может быть загружена DLL. Примером такого модуля может служить Microsoft Transaction Server.

Сервер в виде исполнимого файла

Этот сервер представляет собой обычный исполнимый файл Windows, в котором реализована возможность создания COM-объектов по запросу других приложений. Примером такого сервера может служить пакет Microsoft Office, приложения которого являются COM-серверами.

Регистрация сервера

COM реализует механизм автоматического поиска серверов по запросу клиента. Каждый COM-объект имеет уникальный идентификатор, Class Identifier (CLSID). Windows ведет в реестре базу данных зарегистрированных объектов, индексированную при помощи CLSID. Она расположена в ветке реестра HKEY_CLASSES_ROOT\CLSID.

Для каждого сервера прописывается информация, необходимая для нахождения и загрузки его модуля. Таким образом, клиентское приложение не должно беспокоиться о поиске сервера: достаточно зарегистрировать его на компьютере — и COM автоматически найдет и загрузит нужный модуль. Кроме того, объект может зарегистрировать свое «дружественное» имя, или Programmatic Identifier (PROGID). Обычно оно формируется как комбинация имени сервера и имени объекта, например Word.Application. Это имя содержит ссылку на CLSID объекта. Когда он создается с использованием PROGID, COM просто берет связанное с ним значение CLSID и получает из него всю необходимую информацию.

Серверы в виде исполняемых файлов автоматически регистрируются при первом запуске программы на компьютере. Для регистрации серверов DLL служит программа Regsvr32, поставляемая в составе Windows, либо TRegSvr из поставки DELPHI.

Потоки и «комнаты»

Windows — многозадачная и многопоточная среда с вытесняющей многозадачностью. Применительно к COM это означает, что клиент и сервер могут оказаться в различных процессах или потоках приложения, что к серверу могут обращаться множество клиентов, причем в непредсказуемые моменты времени. Технология COM решает эту проблему при помощи концепции «комнат» (Apartments), в которых и выполняются COM-клиенты и COM-серверы. «Комнаты» бывают однопоточные (Single Threaded Apartment, STA) и многопоточные (Multiple Threaded Apartment, MTA).

При создании однопоточной «комнаты» COM неявно создает окно и при вызове любого метода COM-сервера в этой «комнате» посылает данному окну сообщение при помощи функции PostMessage. Таким образом, организуется очередь вызовов методов, каждый из которых обрабатывается только после того, как будут обработаны все предшествующие вызовы. Основные достоинства однопоточной «комнаты»:

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

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

Недостатки STA напрямую вытекают из ее реализации:

  1. Дополнительные (и иногда излишние) затраты на синхронизацию при вызове методов.
  2. Невозможность отклика на вызов метода, пока не исполнен предыдущий. Например, если в настоящее время выполняется метод, требующий одну минуту на исполнение, то до его завершения COM-объект будет недоступен.

Тем не менее STA, как правило, является наиболее подходящим выбором для реализации COM-сервера. Использовать MTA есть смысл только в том случае, если STA не подходит для конкретного сервера.

Многопоточная «комната» не реализует автоматический сервис по синхронизации и не имеет его ограничений. Внутри нее может быть создано сколько угодно потоков и объектов, причем ни один из объектов не привязан к какому-то конкретному потоку. Это означает, что любой метод объекта может быть вызван в любом из потоков в MTA. В то же самое время в другом потоке может быть вызван любой другой (либо тот же самый) метод COM-объекта по запросу другого клиента. COM автоматически ведет пул потоков внутри MTA, при вызове со стороны клиента находит свободный поток и в нем вызывает метод требуемого объекта. Таким образом, даже если выполняется метод, требующий длительного времени, то для другого клиента он может быть вызван без задержки в другом потоке. Очевидно, что COM-сервер, работающий в MTA, обладает потенциально более высокими быстродействием и доступностью для клиентов, однако он значительно сложнее в разработке, поскольку даже локальные данные объектов не защищены от одновременного доступа и требуют синхронизации.

Передача интерфейсов и параметров

Таким образом, клиент и сервер COM могут выполняться как в одной «комнате», так и в разных, расположенных в различных процессах или даже на разных компьютерах. Возникает вопрос: как же клиент может вызывать методы сервера, если они находятся (в общем случае) в другом адресном пространстве?

Эту работу берет на себя COM. Для доступа к серверу в другой «комнате» клиент должен запросить у COM создание в своей «комнате» представителя, реализующего запрошенный интерфейс. Такой представитель в терминах COM называется proxy и представляет собой объект, экспортирующий запрошенный интерфейс. Одновременно COM создает в «комнате» сервера объект-заглушку (stub), принимающий вызовы от proxy и транслирующий их в вызовы сервера. Таким образом, клиент в своей «комнате» может рассматривать proxy в качестве сервера и работать с ним так, как если бы сервер был создан в его «комнате». В то же время сервер может рассматривать stub как расположенного с ним в одной «комнате» клиента. Всю работу по организации взаимодействия proxy и stub берет на себя COM. При вызове со стороны клиента proxy получает от него параметры, упаковывает их во внутреннюю структуру и передает в «комнату» сервера. Stub получает параметры, распаковывает их и производит вызов метода сервера. Аналогично осуществляется передача параметров обратно. Этот процесс называется Marshalling. При этом «комнаты» клиента и сервера могут иметь разные потоковые модели и физически находиться где угодно. Разумеется, по сравнению с вызовом сервера в своей «комнате» такой вызов требует значительных накладных расходов, однако это единственный способ обеспечить корректную работу любых клиентов и серверов. Если необходимо избежать накладных расходов, сервер надо создавать в той же «комнате», где расположен клиент.

Для обеспечения возможности корректного создания proxy в клиентской «комнате» COM должен узнать «устройство» сервера. Сделать это можно несколькими способами:

  1. Реализовать на сервере интерфейс IMarshal и, при необходимости, — proxy-DLL, которая будет загружена на клиенте для реализации proxy. Подробности реализации описаны в документации COM и MSDN.
  2. Описать интерфейс на языке IDL (Interface Definition Language) и при помощи компилятора MIDL фирмы Microsoft сгенерировать proxy-stub-DLL.
  3. Сделать сервер совместимым с OLE Automation. В этом случае COM сам создаст proxy, используя описание сервера из его библиотеки типов — специального двоичного ресурса, описывающего COM-интерфейс. При этом в интерфейсе можно использовать только типы данных, совместимые с OДУ Automation.
Инициализация COM

Каким же образом клиенты и серверы COM могут создавать «комнаты» в соответствии со своими требованиями? Для этого они должны соблюдать одно правило: каждый поток, желающий использовать COM, должен создать «комнату» при помощи вызова функции CoInitializeEx. Она объявлена в модуле ActiveX.pas следующим образом:

Параметр pvReserved зарезервирован для будущего использования и должен быть равен NIL, а параметр coInit определяет потоковую модель создаваемой комнаты. Он может принимать следующие значения:

COINIT_APARTMENTTHREADED — для потока создается STA. Каждый поток может иметь (или не иметь) свою STA;
COINIT_MULTITHREADED — если в текущем процессе еще не создана MTA, создается новая MTA; если она уже создана другим потоком, поток «подключается» к ранее созданной. Иными словами, каждый процесс может иметь только одну MTA.

Функция возвращает S_OK в случае успешного создания «комнаты».

По завершении работы с COM (или перед завершением работы) поток должен уничтожить «комнату» при помощи вызова процедуры CoUninitialize, также описанной в модуле ActiveX:

Каждый вызов CoInitializeEx должен иметь соответствующий вызов CoUninitialize, то есть, используя COM в приложении, необходимо вызвать CoInitializeEx до первого использования функций COM и CoUninitialize перед завершением работы приложения. VCL реализует автоматическую инициализацию COM при использовании модуля ComObj. По умолчанию создается STA. При желании необходимость использовать другую потоковую модель следует установить флаг инициализации COM до оператора Application.Initialize:

Если COM используется в потоке, то эти функции должны быть вызваны в методе Execute:

Инициализация COM необходима и для вызова любых функций Windows API, связанных с COM, за исключением CoGetMalloc, CoTaskMemAlloc, CoTaskMemFree и CoTaskMemReAlloc.

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

Параметр ThreadingModel может принимать следующие значения:

Apartment — сервер может работать только в STA. Если он создается из STA, то он будет создан в «комнате» вызывающего потока, если из MTA — COM автоматически создаст для него «комнату» c STA и proxy в «комнате» клиента;
Free — сервер может работать только в MTA. Если он создается из MTA, то он будет создан в «комнате» вызывающего потока, если из STA — COM автоматически создаст для него «комнату» c MTA и proxy в «комнате» клиента;
Both — сервер может работать как в STA, так и MTA. Объект всегда создается в вызывающей «комнате».

Если этот параметр не задан, сервер имеет потоковую модель Single. В этом случае он создается в Primary STA (то есть в STA потока, который первым вызвал CoInitialize), даже если создание сервера запрошено из потока, имеющего свою отдельную STA.

Реализация интерфейсов в Delphi

Читайте также:

  1. Аппаратная реализация современных методов несанкционированного доступа к информации
  2. Библиотеки визуальных объектов Delphi
  3. Виды интерфейсов пользователя
  4. Вопрос 4. Предпринимательский доход как реализация экономического интереса предпринимателя
  5. Вопрос 4. РЕАЛИЗАЦИЯ ТЕОРИИ ПОЭТАПНОГО ФОРМИРОВАНИЯ УМСТВЕННЫХ ДЕЙСТВИЙ (П.Я. ГАЛЬПЕРИН, Н.Ф. ТАЛЫЗИНА, М.Б. ВОЛОВИЧ).
  6. Выбор интерфейсов измерительных систем
  7. Графические возможности Delphi
  8. Краткий обзор интерфейсов.
  9. Лекция № 16. Реализация управляющего алгоритма
  10. Личностно ориентированный подход и его реализация в современном педагогическом процессе. Индивидуализация и дифференциация как компоненты личностно ориентированного подхода.
  11. Матричная реализация булевых функций.
  12. Методы воспитания и их реализация в деятельности культуролога.

Реализация интерфейса в Delphi всегда осуществляется в классе. Для этого в объявлении класса необходимо указать, какие интерфейсы он реализует.

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

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

TTest = class(TInterfacedObject, ITest)

destructor Destroy: override;

MessageBox(0,’TTest.Destroy’, nil, 0);

Здесь класс TTest реализует интерфейс ITest. Рассмотрим использование интерфейса из программы:

procedure TForm1.Button1Click(Sender: TObject);

В этом примере можно увидеть некоторые особенности поддержки СОМ-технологий в Delphi.

Во-первых, оператор присваивания при приведении типа данных к интерфейсу неявно вызывает метод _AddRef. При этом количество ссылок на интерфейс увеличивается на 1.

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

Если нужно уничтожить реализацию интерфейса немедленно, не дожидаясь выхода переменной за область видимости следует присвоить ей значение nil:

Test := nil; // Неявно вызываются IUnknown._Release

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

· при приведении типа объекта к интерфейсу вызывается метод _AddRef;

· при выходе переменной, ссылающейся на интерфейс, за область видимости, а также при присвоении ей другого значения вызывается метод _Release;

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

В рассмотренных примерах код для получения интерфейса у класса генерировался (с проверкой типов) на этапе компиляции. Если класс не реализует требуемого интерфейса, то программа не компилируется. Однако существует возможность запросить интерфейс и во время выполнения программы. Для этого служит оператор as, который вызывает метод QueryInterface и, в случае успеха, возвращает ссылку на полученный интерфейс. В противном случае генерируется исключение.

Например, следующий код будет успешно откомпилирован, но при выполнении вызовет ошибку Interface not supported (интерфейс не поддерживается):

Test := TInterfacedObject.Create as Itest;

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

Test := TTest.Create as ITest;

Проведем сравнение интерфейсов с классами, сведения о которых известны из объектно-ориентированного программирования. Для интерфейсов, так же как и для классов, определено понятие иерархии. Суть этого понятия: каждый класс (или интерфейс) может иметь одного или нескольких потомков. Во всех потомках данного класса (интерфейса) сохраняются все методы (и данные для классов), которые имеются у родителей. В Delphi классы (точно так же, как и интерфейсы) могут иметь только одного родителя, поэтому они образуют иерархическое дерево. Его можно увидеть в Delphi при выборе команды View > Browser в меню среды разработки. В вершине иерархического дерева классов находится родоначальник всех классов — TObject, а интерфейсов — Unknown. Все остальные потомки содержат методы TObject (IUnknown).

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

Абстрактным методом называется виртуальный или динамический метод, который объявлен, но не реализован. Классы, в которых имеются абстрактные методы, называются абстрактными классами. При объявлении абстрактного метода в таблицу виртуальных (динамических) методов заносится значение nil. При попытке вызвать абстрактный метод из экземпляра такого класса генерируется исключение EAbstractError.

Абстрактные методы требуют их перекрытия в потомках данного класса. Например, пусть имеется абстрактный класс TStream, в котором определены два абстрактных метода — Read и Write. В потомках этого класса — TMemoryStream и TFileStream — эти методы реализованы так, что происходит чтение (запись) данных в память для первого класса и в файл — для второго. Поэтому достаточно написать всего одну процедуру для сохранения данных в поток данных (stream), при этом параметром этого метода является объект типа TStream. Далее, при вызове этой процедуры с объектом TMemoryStream все данные запишутся в память, а с объектом TFileStream — в файл.

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

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

Таким образом, вызов методов интерфейса осуществляется следующим образом. Интерфейс создается на сервере, а клиент получает на него ссылку. После этого клиент для вызова метода QueryInterface производит следующие операции.

1. В стек помещаются адрес, где находится переменная HResult, и адреса переменных Р и IID.

2. На основании полученной ссылки на интерфейс вычисляется адрес таблицы виртуальных методов.

3. Находится третий столбец данной таблицы (поскольку метод QueryInterface реализован третьим).

4. Из таблицы виртуальных методов извлекается адрес метода и ему передается управление.

5. После возвращения управления очищается стек.

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

| следующая лекция ==>
Интерфейс IUnknown | Интерфейс ICIassFactory и использование системного реестра

Дата добавления: 2014-01-05 ; Просмотров: 710 ; Нарушение авторских прав? ;

Нам важно ваше мнение! Был ли полезен опубликованный материал? Да | Нет

Интерфейсы

Добрый день, специалисты.

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

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

11.12.2020, 14:17

интерфейсы
ЗДравствуйте! Нужна помощь по интерфейсам. Меня интересует 1) Что вообще из себя представляет.

GDI+ Интерфейсы
Итак рассмотрим 2 кода с GDI+ Используется вот этот класс http://www.bilsen.com/gdiplus Код 1.

Интерфейсы и классы
Добрый день! у меня есть маленькая проблема. Суть проблемы: есть общий интерфейс и классы для.

Интерфейсы (ООП)
Здравствуйте! Сколько занимаюсь программирование так и не понял зачем нужны интерфейсы в ООП. Кто.

Классы vs Интерфейсы, что вы используете и почему?
Привет, всем вопрос в теме. Мой ответ — классы, потому что побаивался интерфейсов, не знал всех.

11.12.2020, 17:17 2

Добавлено через 1 минуту
от GunSmoker^

Множественное наследование и интерфейсы
Но здесь, однако, кроется интересное для меня наблюдение. Если вы заметили, то объекты хорошо позволяют «реализовать что-то». У них есть наследование (и интерфейса и реализации), скрытие и дружественность, и т.д. Всё это позволяет легко реализовывать функциональность. Но не всегда это идеально удобно с точки зрения потребления/использования. Здесь уже ограничители (наследование, скрытие, . ) начинают скорее мешаться, чем приносить пользу. В самом деле, если я хочу добавить элемент в коллекцию, зачем мне всенепременно наследовать его от нужного класса? А если мне нужно добавить в коллекцию класс вне её дерева наследования? Ведь, по сути, здесь было бы достаточным, чтобы элемент поддерживал определённую функциональность (читай: «набор методов управления»). Для этого вовсе не обязательно строго наследовать от предопределённого класса, поскольку от элемента требуется лишь удовлетворять набору условий.

Насколько я понимаю, для решения этой проблемы (в том числе) в других языках программирования вводится поддержка множественного наследования: один класс может удовлетворять сразу нескольким функциональностям. К примеру, студент может быть не только человеком, но и рабочим, и музыкантом, и членом семьи (т.е. наследоваться от них в терминах множественного наследования). При этом, если наследуемые классы, в свою очередь, также наследуются друг от друга в различных комбинациях, то могут возникать конфликты (например, если «музыкант» и «член семьи» наследуют «прозвище» от «человек» с замещением реализации, то какую из реализаций должен брать «студент»?). Само собой, языки с поддержкой множественного наследования также содержат механизмы разрешения таких конфликтов. В целом же, множественное наследование часто критикуется. Проблемы, его чрезмерная сложность приводят критиков к выводу, что от него больше вреда, чем пользы. При этом также звучит мнение, что множественное наследование (как расширение «простого» одиночного наследования) — ошибочная концепция, порождённая неверным анализом и проектированием.

В Delphi проблемы множественного наследования решаются интерфейсами. Интерфейс — это полностью абстрактный класс, все методы которого виртуальны и абстрактны. Иными словами, говоря грубо, интерфейс — это запись (record) с указателями на функции. Каждый метод интерфейса должен быть реализован в классе. Причём реализацию вы либо пишете сами, либо наследуете, либо делегируете (для случая агрегации). Иными словами, любой интерфейс реализуется классом без конфликтов. Плюс, в каждый момент времени вы работаете с одним конкретным интерфейсом, а не с «объединённым набором интерфейсов», поэтому проблемы выбора нужной реализации тут просто нет — будет использоваться метод используемого интерфейса. Не та реализация? Берём другой (нужный) интерфейс с нужной реализацией.

Также замечу, что в Delphi отсутствует возможность множественного наследования не только классов, но и интерфейсов (множественное наследование интерфейсов существенно проще множественного наследования классов, поскольку интерфейсы не содержат реализации; тем не менее, даже здесь есть одна возможность для конфликтов).

В целом же, я считаю, что использование интерфейсов — это наиболее правильный вариант реализации множественного наследования, поскольку явное указание реализации исключает конфликты. Введение понятия интерфейсов является компромиссом, позволяющим получить преимущества множественного наследования, не реализуя его в полном объёме и, таким образом, не сталкиваясь со специфичными для него сложностями. Именно такой подход принят во многих современных языках — не только в Delphi, но и, к примеру, C# или Java.

Илон Маск рекомендует:  Стеки гипертекстовых ссылок
Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL