Реализация делегирования интерфейса Delphi не может вызывать напрямую
Я знаю, что это старый и я использую Delphi 5. Не уверен, что это то же самое в более поздней версии Delphi.
Я обнаружил, что если объект делегирует реализацию своего интерфейса другому объекту, исходный объект не может напрямую вызывать интерфейсный метод, что, как я ожидаю, должно быть реализацией интерфейса делегирования.
Вот мой вопрос
IFoo = interface
procedure InterfaceMethod;
end;
TBar = class(TComponent, IFoo)
FFoo: IFoo;
procedure ObjectMethod;
property Foo: IFoo read FFoo implements IFoo;
end;
TBar.Method2
begin
InterfaceMethod; // this will give compile error, Method1 not declared!
(Self as IFoo).InterfaceMethod; // compile error, saying operation not supported.
FFoo.InterfaceMethod; // this work, but meaningless, since this can be any object, why the need of interface!
end;
Похоже, если я делаю это вне класса TBar. Сказать
Bar := TBar.Create;
Bar.InterfaceMethod; // compile error
(Bar as IFoo).InterfaceMethod; // compile error
Bar.Foo.Method1; // this work, but...
Может кто-нибудь объяснить, почему это так, или мое понимание не правильно. Или может быть это будет правильно после D5?
Ниже приведен пример кода
unit iwSqlDbEngine;
//
// Database resource and helper funcions.
//
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
Db, ScktComp, ExtCtrls;
type
ISqlMaker = interface
['{5A671E7B-957B-4A52-BB5E-87EEA8E5687B}']
procedure SetSqlStatement(const value: string);
function GetSqlStatement: string;
// Insert and update
function Insert(table: string): ISqlMaker;
function Update(table: string): ISqlMaker;
function AddField(fieldname: string; value: variant; isExpr: Boolean = False): ISqlMaker; overload;
function AddField(field: TField): ISqlMaker; overload;
// Delete
function Delete(table: string): ISqlMaker;
// Select
function Select(table: string; columns: array of string): ISqlMaker;
function Join(table: string; columns, matches: array of string): ISqlMaker;
function LeftJoin(table: string; columns, matches: array of string): ISqlMaker;
// Other clauses
function Where(aExpr: string): ISqlMaker;
function GroupBy(columns: array of string): ISqlMaker;
function OrderBy(columns: array of string): ISqlMaker;
function Having(vExprs: array of string): ISqlMaker;
property SqlStatement: string read GetSqlStatement write SetSqlStatement;
end;
TSampleDbEngine = class(TComponent, ISqlMaker)
private
FSqlMaker: ISqlMaker;
public
procedure Execute;
property SqlMaker: ISqlMaker read FSqlMaker write FSqlMaker implements ISqlMaker;
end;
implementation
procedure TSampleDbEngine.Execute;
begin
(Self as ISqlMaker).GetSqlStatement;
end;
end.
Та же ошибка компиляции, как описано выше.
2 ответа
Вы получаете ошибку InterfaceMethod not declared
потому что в контексте метода TBar, область по умолчанию (self
) ТБар, а не ИФУ.
Если TBar реализует IFoo напрямую, класс TBar (как правило) будет иметь метод, который вы также пытаетесь вызвать. то есть, есть ли у вас ссылка на TBar или ссылка на IFoo, оба поддерживают этот метод. Исключением из этого может быть, если у вас есть условие разрешения метода интерфейса в TBar для реализации определенного метода интерфейса с некоторым методом другого имени в реализующем классе.
Также могут быть различия за пределами области действия TBar, если реализация метода интерфейса не имеет общедоступной области. Все методы интерфейса являются общедоступными, поэтому, если реализация конкретного метода является частной или защищенной, ссылка TBar не может использоваться для вызова этого метода интерфейса, например, только IFoo.
Но в вашем случае весь интерфейс делегирован, и поэтому методы просто не существуют в самом классе TBar.
Вы должны быть в состоянии набрать тип self
к требуемому типу интерфейса, но нет смысла использовать QueryInterface, вызовы AddRef / Release и другие необходимые скаффолдинги только для того, чтобы сделать это возможным.
У вас уже есть ссылка на интерфейс, который вам нужен. Вы можете просто позвонить напрямую:
procedure TBar.ObjectMethod
begin
fFoo.InterfaceMethod; // fFoo holds the delegated interface so if the TBar
// needs to call the delegated interface, just call it!
end;
То же самое относится и к вашему TSampleDbEngine, который аналогично уже имеет обязательную ссылку на делегированный интерфейс:
procedure TSampleDbEngine.Execute;
begin
fSqlMaker.GetSqlStatement;
end;
В комментариях ниже вы описываете, что ваша реальная задача - обеспечить согласованный интерфейс [sic] для вашей реализации, при этом пользователю не нужно беспокоиться о том, имеют ли они ссылку на объект или на интерфейс, который реализует объект.
Я думаю, это наводит на мысль о неправильном понимании того, что представляет собой интерфейс.
Если код имеет только ссылку на объект, он может вызывать только методы, определенные для этого типа объекта.
o: TFoo;
o.ObjectMethod; // This is OK, o is a TFoo reference
o.Bar; // This is NOT OK. o is NOT an IFoo reference
Путаница может возникнуть, если TFoo напрямую и публично реализует метод Bar, требуемый интерфейсом IFoo:
TFoo = class(TObject, IFoo)
public
procedure Bar;
end;
o: TFoo;
o.Bar; // This is fine, but o is still a TFoo.
// IFoo is completely irrelevant here,
// it may as well not exist.
Это может создать впечатление, что вы используете интерфейс IFoo от TFoo. Но вы не.
Просто метод, который удовлетворяет этому интерфейсу, также общедоступен через объектную ссылку на объект реализации. Это может быть проиллюстрировано тем фактом, что если этот метод сделан закрытым, интерфейс все еще выполняется и может использоваться через ссылку на интерфейс, но больше не доступен через ссылку на объект:
TFoo = class(TObject, IFoo)
private
procedure Bar;
end;
o: TFoo;
i: IFoo;
i := o as IFoo;
i.Bar; // This is fine. Interface methods are always public
o.Bar; // Once again, this will NOT compile because TFoo.Bar is now private
Очевидно, что со ссылкой на интерфейс потребитель ссылки не знает и не заботится о том, был ли этот интерфейс непосредственно реализован объектом или был делегирован.
Обратите внимание, что это ничем не отличается от ситуации, когда объект может реализовывать ДВА (или более) различных интерфейсов. Вы не можете вызывать методы одного интерфейса, используя ссылку на другой.
Вы должны получить правильный тип ссылки, чтобы использовать ссылку этого типа.
Таким образом, ваша настоящая проблема заключается в том, что ваша реализация позволяет пользователю получать ссылки неправильного типа с самого начала.
Предположительно, у вас есть конструктор, который, конечно, дает ссылку на объект, но при этом существует предположение, что потребитель этого объекта должен преобразовать его в ссылку на интерфейс, но это предположение не является принудительным (или принудительным, как реализовано).
Вы можете помочь обеспечить это, не предоставляя общедоступный конструктор, а вместо этого предоставляя метод фабрики, который только дает ссылку на интерфейс и вызывает исключение, если кто-то неправильно пытается создать экземпляр класса напрямую:
type
IFoo = interface
procedure Bar;
end;
TFoo = class(TInterfacedObject, IFoo)
private
procedure Bar;
constructor InternalCreate;
public
class function NewFoo: IFoo;
constructor Create;
end;
procedure TFoo.Bar;
begin
end;
constructor TFoo.InternalCreate;
begin
inherited Create;
end;
constructor TFoo.Create;
begin
raise ENotSupportedException.Create('Do not create instances of TFoo. Use the factory method(s) provided');
end;
class function TFoo.NewFoo: IFoo;
begin
result := TFoo.InternalCreate; // We can call our own private, internal constructor
end;
Теперь потребители вашего класса могут получить только интерфейсную ссылку на объект, который реализует интерфейс, и поэтому не могут попасть в затруднительную ситуацию, когда их путают методы класса по сравнению с методами интерфейса и любые различия, которые могут существуют из-за видимости метода, делегирования интерфейса или чего-то еще, с чем они действительно не должны быть связаны в первую очередь.
В общих чертах: не пытайтесь - и не поощряйте потребителей вашего кода к попыткам - смешивать интерфейсы и ссылки на объекты на один и тот же объект.
Помимо этих проблем, есть и другие, более серьезные проблемы, с которыми вы, вероятно, столкнетесь из-за отсутствия подсчета ссылок на ссылки на объекты.
Вы получаете сообщение об ошибке "InterfaceMethod не объявлен", потому что ваш Bar
переменная имеет тип TBar
не IFoo
и так как TBar
не имеет InterfaceMethod
компилятор прав. Внутри TBar.Method2
Self
имеет тип TBar
вот почему это тоже не работает.
Идея бросить как IFoo
правильно, но это не так, как as
Оператор требует, чтобы интерфейс имел GUID, который вы IFoo
не имеет Поставь карет сразу после IFoo = interface
и нажмите Ctrl+G, чтобы сгенерировать GUID для интерфейса.
У меня нет доступа к Delphi 5, но я только что протестировал с Delphi 7, который должен быть очень похож на D5 со следующим кодом
IFoo = interface
['{B6AFF17B-C239-414D-84DE-F8F2AE1FE4E0}']
procedure Bar;
end;
TFoo = class(TComponent, IFoo)
private
FFoo: IFoo;
public
property Foo: IFoo read FFoo implements IFoo;
end;
и теперь "вне класса" оба
var f: TFoo;
begin
f := TFoo.Create(nil);
(f as IFoo).Bar;
end;
а также
var f: IFoo;
begin
f := TFoo.Create(nil);
f.Bar;
end;
компилировать просто отлично.