【问题标题】:Delphi interface delegation implements cannot call directlyDelphi接口委托实现不能直接调用
【发布时间】:2023-03-29 07:25:02
【问题描述】:

我知道这是旧的,我使用的是 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.

与上述相同的编译错误。

【问题讨论】:

  • 你能不能提供一个minimal reproducible example来代替这个伪代码
  • 以上是可以编译的最少代码。抱歉,Method1 指的是 InterfaceMethod,Method2 是 ObejctMethod。我在初稿中使用 Method1 和 Method2,认为名称更改更清晰。 TBar.Method2 应该是过程 TBar.ObjectMethod;
  • 不,不是。我们不知道变量的类型,并且大部分语法都是由组成的。请加倍努力。如果问题得到解决,这个问题将得到一个很好的答案。
  • 您的示例代码使情况变得更糟。清楚地表明原始代码是假的。请点击我第一条评论中的链接并仔细阅读。一个好的minimal reproducible example 会产生巨大的影响。

标签: delphi delphi-5


【解决方案1】:

您收到错误“未声明接口方法”,因为您的 Bar 变量的类型为 TBar,而不是 IFoo,并且由于 TBar 没有 InterfaceMethod,编译器是正确的。在TBar.Method2 内部,SelfTBar 类型,所以这就是它在那里不起作用的原因。

转换为IFoo 的想法是正确的,但它失败了,因为as 运算符要求接口具有您IFoo 没有的GUID。在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;

编译就好了。

【讨论】:

  • 我只是尝试给界面提供GUID,结果是一样的。
  • [错误] iwSqlDbEngine.pas(198):运算符不适用于此操作数类型
  • 顺便说一下,如果我定义一个变量say x: IFoo,那么x := Self,然后使用x 调用该方法。它可以编译。但是,如果在课堂之外,这不起作用。那就是我不能声明一个接口变量并分配一个对象或在赋值中强制转换一个对象。我需要使用 QueryInterface 吗?
  • 在课外时,你 f := TBar.Create(nil) 了吗?无论如何,它在 D5 中不起作用。感谢您的确认。我认为这是 D5 中的一个问题。
【解决方案2】:

您收到错误InterfaceMethod not declared,因为在 TBar 方法的上下文中,默认范围 (self) 是 TBar,而不是 IFoo。

如果 TBar 直接实现了 IFoo,那么 TBar 类(通常)也会有您尝试调用的方法。即,无论您有 TBar 引用还是 IFoo 引用,两者都将支持该方法。如果您在 TBar 上有一个接口 method resolution clause 来实现一个特定的接口方法,并且在实现类中使用另一个名称的某个方法,则可能会有一个例外。

如果接口方法的实现没有 public 范围,TBar 范围之外也可能存在差异。所有接口方法都是public,所以如果特定方法的实现是privateprotected,那么TBar引用不能用于调用该接口方法,例如只能调用 IFoo

但在您的情况下,整个接口是委托的,因此 TBar 类本身上根本不存在这些方法。

您应该能够将self 类型转换为所需的接口类型,但没有必要引发 QueryInterfaceAddRef/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;

在下面的 cmets 中,您描述了您真正关心的是为您的实现提供一致的接口 [原文如此],而用户不必担心他们是否引用了对象或对象实现的接口。

我认为这表明对接口的含义存在误解。

如果代码只有一个对象引用,那么它只能调用为该类型对象定义的方法。

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 直接公开地实现 IFoo 接口所需的 Bar 方法,可能会引起混淆:

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.

这可以使您看起来正在使用 TFooIFoo 接口。但你不是。

只是满足接口的方法恰好可以通过对实现对象的对象引用公开访问。这可以通过以下事实来说明:如果将该方法设置为 private,接口仍然可以满足并且可以通过接口引用使用,但不再可以通过对象引用访问:

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

显然,对于接口引用,引用的使用者不知道也不关心该接口是由对象直接实现还是被委托。

请注意,这与对象可能实现两个(或更多)不同接口的情况没有什么不同。您不能使用对另一个接口的引用来调用一个接口的方法。

您必须获得正确的引用类型才能使用该类型的引用。

所以你真正的问题是你的实现允许用户从一开始就获得错误类型的引用。

假设您有一个构造函数,它当然会产生一个对象引用,但是假设该对象的使用者应该将其转换为接口引用,但是这个假设没有被强制执行(或可强制执行,如已实现)。

您可以帮助通过不提供公共构造函数来强制执行此操作,而是提供一个仅产生接口引用并在有人错误地尝试直接实例化类时抛出异常的工厂方法: p>

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;

现在您的类的消费者只能获得对实现该接口的对象的接口引用,因此不会陷入一个棘手的境地,即他们被类上的方法与接口上的方法以及任何由于方法可见性、接口委托或其他任何他们一开始就不应该关心的事情而可能存在的差异。

一般而言: 不要尝试 - 也不鼓励代码的使用者尝试 - 将接口和对象引用混合到同一个对象。

除了这些问题之外,您可能还会遇到其他更严重的问题,即对象引用缺少引用计数。

【讨论】:

  • 谢谢,我明白我可以直接在类中使用接口属性,而不是输入 case Self。当创建具有接口委托实现的对象时,我的问题实际上出在类外部。我必须通过委托属性调用接口方法。从对象编程的角度来看,我想对用户隐藏实现。因此,如果用户使用 Object 接口方法,则用户不需要知道接口是由 Class 本身实现还是被委托。调用 Object 接口方法的代码应该是一样的。
  • (对不起,忽略以上内容,无法编辑。)我明白我可以通过委托属性调用接口方法。但我的问题是这是为什么设计的界面?接口的想法是对用户隐藏实现。因此,如果用户使用 Object 接口方法,则用户不需要知道该接口是由 Class 本身实现还是被委托。调用 Object 接口方法的代码应该是一样的。
  • 如果你对实现对象的引用是一个接口引用,你可以。如果您仅将对象的引用作为一个类,那么您当然只能调用该类的方法,就像您持有对一个接口的引用一样,您不能调用底层实现对象可能实现的其他接口上的方法。如果您不希望接口的使用者使用类引用,请不要让他们获得。提供一个只产生一个接口引用的工厂方法。
  • 谢谢。但我对对象添加接口的理解是增强对象的能力。否则,我们可以定义两个不同的类。并将第二类的对象放入第一类。就像委托一样,但不需要接口定义。我也知道参考计算问题。但我有点同意一篇文章建议根本不要使用引用计数。
  • 向 Deltics 购买很多东西。对于我的问题的答案,下面已经确认它是 D5 的问题。所以我的问题得到了回答。但是对于使用Interface的概念,如果interface的Delphi实现对我有用,我想这应该是另一个问题了。
猜你喜欢
  • 2011-03-29
  • 2020-11-10
  • 1970-01-01
  • 2011-02-07
  • 2013-04-19
  • 1970-01-01
  • 2019-08-05
  • 1970-01-01
  • 2010-11-19
相关资源
最近更新 更多