Delphi:如何将接口实现委托给子对象?

12
我有一个对象,它将特别复杂的接口实现委托给一个子对象。我认为这正是`TAggregatedObject`的工作。这个“子”对象维护着对其“控制器”的弱引用,并将所有的 `QueryInterface` 请求传递回父级。这保持了`IUnknown`始终是相同的对象的规则。
所以,我的父级(即“控制器”)对象声明它实现了 `IStream` 接口:
type
   TRobot = class(TInterfacedObject, IStream)
   private
      function GetStream: IStream;
   public
      property Stream: IStream read GetStrem implements IStream;
   end;

注意:这只是一个假想的例子。我选择了单词“机器人”(Robot),因为它听起来很复杂,但只有5个字母 - 它很短。我还选择了“ IStream”,因为它也很短。我原本想使用“IPersistFile”或“ IPersistFileInit”,但它们更长,并且会使示例代码更难读懂。换句话说:这是一个虚构的例子。
现在我有一个将实现“IStream”的子对象:
type
   TRobotStream = class(TAggregatedObject, IStream)
   public
      ...
   end;

剩下的就是我的问题所在了:当需要创建RobotStream时:

function TRobot.GetStream: IStream;
begin
    Result := TRobotStream.Create(Self) as IStream;
end;

这段代码无法编译,出现错误:运算符不适用于此操作数类型。

这是因为delphi尝试在一个没有实现IUnknown的对象上执行as IStream操作:

TAggregatedObject = class
 ...
   { IUnknown }
   function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
   function _AddRef: Integer; stdcall;
   function _Release: Integer; stdcall;
 ...
方法可能存在,但对象没有宣传其支持IUnknown。没有IUnknown接口,Delphi 无法调用QueryInterface进行强制转换。

因此,我更改了我的TRobotStream类,以宣传其实现了缺失的接口(它确实是继承自其祖先):

type
   TRobotStream = class(TAggregatedObject, IUnknown, IStream)
   ...

现在它已编译成功,但在运行时崩溃于该行代码:
Result := TRobotStream.Create(Self) as IStream;

现在我可以看到发生了什么,但是我无法解释为什么。Delphi在子对象构造函数退出时调用父对象Robot上的IntfClear

我不知道避免这种情况的正确方式。我可以尝试强制转换:

Result := TRobotStream.Create(Self as IUnknown) as IStream;

希望它能保持一个引用,结果它确实保留了该引用 - 在构造函数退出时没有崩溃。
注意:这让我感到困惑。由于我传递的是一个对象而不是接口,我会认为编译器会隐式地执行类型转换,即:
Result := TRobotStream.Create(Self as IUnknown);
以满足调用。语法检查器没有抱怨让我觉得一切都正确。
但崩溃没有结束。我将行改为:
Result := TRobotStream.Create(Self as IUnknown) as IStream;

这段代码确实从TRobotStream的构造函数中返回,而没有销毁我的父对象,但现在我遇到了堆栈溢出。

原因是TAggregatedObject将所有QueryInterface(即类型转换)都延迟到父对象。在我的情况下,我将一个TRobotStream强制转换为一个IStream

当我在下面代码结束后询问TRobotStreamIStream时:

Result := TRobotStream.Create(Self as IUnknown) as IStream;

它会向其控制器请求IStream接口,这会触发以下调用:
Result := TRobotStream.Create(Self as IUnknown) as IStream;
   Result := TRobotStream.Create(Self as IUnknown) as IStream;

这会转而调用:

Result := TRobotStream.Create(Self as IUnknown) as IStream;
   Result := TRobotStream.Create(Self as IUnknown) as IStream;
      Result := TRobotStream.Create(Self as IUnknown) as IStream;

砰!堆栈溢出。


盲目地,我尝试删除到IStream 的最终转换,让Delphi尝试将对象隐式转换为接口(正如我刚才看到的一样不起作用):

Result := TRobotStream.Create(Self as IUnknown);

现在没有崩溃了,这让我不太理解。我构建了一个支持多个接口的对象。那么 Delphi 现在是如何知道要转换接口的?它是否执行了适当的引用计数?我看到上面说它没有。是否存在潜在的微妙错误会导致客户端崩溃?
现在留下了四种可能的调用方式来调用我的一行代码。哪一种是有效的?
1. `Result := TRobotStream.Create(Self);` 2. `Result := TRobotStream.Create(Self as IUnknown);` 3. `Result := TRobotStream.Create(Self) as IStream;` 4. `Result := TRobotStream.Create(Self as IUnknown) as IStream;`
真正的问题是什么?
我遇到了很多微妙的错误和难以理解的编译器细节。这使我相信我做错了一切。如果需要,请忽略我说的一切,并帮助我回答问题:
委托接口实现给子对象的正确方式是什么?
也许我应该使用 TContainedObject 而不是 TAggregatedObject。也许两者配合使用,父对象应该是 TAggregatedObject,而子对象是 TContainedObject。也许反过来。也许在这种情况下没有一个适用。
请注意:可以忽略我所说的主要部分中的一切。这只是为了表明我已经思考过这个问题。有些人会认为,通过包括我已经尝试过的内容,我已经破坏了可能的回答;人们可能会关注我的失败问题,而不是回答我的问题。真正的目标是将接口实现委托给子对象。这个问题包含了我详细尝试使用 TAggregatedObject 解决问题的情况。您甚至看不到我其他两种解决方案模式。其中一种受循环引用计数困扰,并且违反了 IUnknown 等价规则。
编辑:语法纠正
编辑2:没有机器人控制器这种东西。好吧,有的——我一直在使用 Funuc RJ2 控制器。但在这个例子中没有!
  TRobotStream = class(TAggregatedObject, IStream)
    public
        { IStream }
     function Seek(dlibMove: Largeint; dwOrigin: Longint;
        out libNewPosition: Largeint): HResult; stdcall;
     function SetSize(libNewSize: Largeint): HResult; stdcall;
     function CopyTo(stm: IStream; cb: Largeint; out cbRead: Largeint; out cbWritten: Largeint): HResult; stdcall;
     function Commit(grfCommitFlags: Longint): HResult; stdcall;
     function Revert: HResult; stdcall;
     function LockRegion(libOffset: Largeint; cb: Largeint; dwLockType: Longint): HResult; stdcall;
     function UnlockRegion(libOffset: Largeint; cb: Largeint; dwLockType: Longint): HResult; stdcall;
     function Stat(out statstg: TStatStg; grfStatFlag: Longint): HResult; stdcall;
     function Clone(out stm: IStream): HResult; stdcall;

     function Read(pv: Pointer; cb: Longint; pcbRead: PLongint): HResult; stdcall;
     function Write(pv: Pointer; cb: Longint; pcbWritten: PLongint): HResult; stdcall;
  end;

  TRobot = class(TInterfacedObject, IStream)
  private
      FStream: TRobotStream;
      function GetStream: IStream;
  public
     destructor Destroy; override;
      property Stream: IStream read GetStream implements IStream;
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

procedure TForm1.Button1Click(Sender: TObject);
var
    rs: IStream;
begin
    rs := TRobot.Create;
    LoadRobotFromDatabase(rs); //dummy method, just to demonstrate we use the stream
    rs := nil;
end;

procedure TForm1.LoadRobotFromDatabase(rs: IStream);
begin
    rs.Revert; //dummy method call, just to prove we can call it
end;

destructor TRobot.Destroy;
begin
  FStream.Free;
  inherited;
end;

function TRobot.GetStream: IStream;
begin
  if FStream = nil then
     FStream := TRobotStream.Create(Self);
  result := FStream;
end;

问题在于“父”TRobot对象在以下调用期间被销毁:
FStream := TRobotStream.Create(Self);
2个回答

10

你需要为创建的子对象添加一个字段实例:

type
  TRobot = class(TInterfacedObject, IStream)
  private
     FStream: TRobotStream;
     function GetStream: IStream;
  public
     property Stream: IStream read GetStream implements IStream;
  end;

destructor TRobot.Destroy;
begin
  FStream.Free; 
  inherited; 
end;

function TRobot.GetStream: IStream;
begin
  if FStream = nil then 
    FStream := TRobotStream.Create(Self);
  result := FStream;
end;

更新 TRobotStream应该派生自TAggregatedObject,就像你已经猜到的那样。声明应该是:

TAggregatedObject是一个基类,它提供了一种在TObject中聚合其他对象的方法。通过继承TAggregatedObject并将此对象作为参数传递给构造函数,可以实现这种聚合行为。因此,在这个例子中,TRobotStream需要从TAggregatedObject继承以实现所需的功能。
type
  TRobotStream = class(TAggregatedObject, IStream)
   ...
  end;

不必提及IUnknown。

在TRobot.GetStream中,result := FStream这一行进行了隐式类型转换FStream as IStream,所以也不需要写出来。

FStream必须声明为TRobotStream而不是IStream,以便在销毁TRobot实例时可以销毁它。注意:TAggregatedObject没有引用计数,因此容器必须管理其生存期。

更新(Delphi 5代码):

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, activex, comobj;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Edit1: TEdit;
    procedure Button1Click(Sender: TObject);
  private
    procedure LoadRobotFromDatabase(rs: IStream);
  public
  end;

type
  TRobotStream = class(TAggregatedObject, IStream)
  public
    { IStream }
    function Seek(dlibMove: Largeint; dwOrigin: Longint;
       out libNewPosition: Largeint): HResult; stdcall;
    function SetSize(libNewSize: Largeint): HResult; stdcall;
    function CopyTo(stm: IStream; cb: Largeint; out cbRead: Largeint; out cbWritten: Largeint): HResult; stdcall;
    function Commit(grfCommitFlags: Longint): HResult; stdcall;
    function Revert: HResult; stdcall;
    function LockRegion(libOffset: Largeint; cb: Largeint; dwLockType: Longint): HResult; stdcall;
    function UnlockRegion(libOffset: Largeint; cb: Largeint; dwLockType: Longint): HResult; stdcall;
    function Stat(out statstg: TStatStg; grfStatFlag: Longint): HResult; stdcall;
    function Clone(out stm: IStream): HResult; stdcall;
    function Read(pv: Pointer; cb: Longint; pcbRead: PLongint): HResult; stdcall;
    function Write(pv: Pointer; cb: Longint; pcbWritten: PLongint): HResult; stdcall;
  end;

type
  TRobot = class(TInterfacedObject, IStream)
  private
    FStream: TRobotStream;
    function GetStream: IStream;
  public
    destructor Destroy; override;
    property Stream: IStream read GetStream implements IStream;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
var
  rs: IStream;
begin
  rs := TRobot.Create;
  LoadRobotFromDatabase(rs); //dummy method, just to demonstrate we use the stream
  rs := nil;
end;

procedure TForm1.LoadRobotFromDatabase(rs: IStream);
begin
  rs.Revert; //dummy method call, just to prove we can call it
end;

function TRobotStream.Clone(out stm: IStream): HResult;
begin
end;

function TRobotStream.Commit(grfCommitFlags: Integer): HResult;
begin
end;

function TRobotStream.CopyTo(stm: IStream; cb: Largeint; out cbRead, cbWritten: Largeint): HResult;
begin
end;

function TRobotStream.LockRegion(libOffset, cb: Largeint; dwLockType: Integer): HResult;
begin
end;

function TRobotStream.Read(pv: Pointer; cb: Integer; pcbRead: PLongint): HResult;
begin
end;

function TRobotStream.Revert: HResult;
begin
end;

function TRobotStream.Seek(dlibMove: Largeint; dwOrigin: Integer;
  out libNewPosition: Largeint): HResult;
begin
end;

function TRobotStream.SetSize(libNewSize: Largeint): HResult;
begin
end;

function TRobotStream.Stat(out statstg: TStatStg; grfStatFlag: Integer): HResult;
begin
end;

function TRobotStream.UnlockRegion(libOffset, cb: Largeint; dwLockType: Integer): HResult;
begin
end;

function TRobotStream.Write(pv: Pointer; cb: Integer; pcbWritten: PLongint): HResult;
begin
end;

destructor TRobot.Destroy;
begin
  FStream.Free;
  inherited;
end;

function TRobot.GetStream: IStream;
begin
  if FStream = nil then
     FStream := TRobotStream.Create(Self);
  result := FStream;
end;

end.

1
@Marco:你的担忧是正确的,但请仔细查看TAggregatedObject及其描述。它的接口引用计数被委托给容器。不仅可以保留对对象实例的引用,而且在容器销毁期间销毁对象是必要的。问题描述中所述的问题正是TAggregatedObject的用途。只是使用方法不正确。 - Uwe Raabe
好的,仍然有些问题。使用您的声明,尝试调用 var rs: IStream; rs := TRobot.Create; 在创建 TRobotStream 时,引用计数返回到零,销毁了自身的 TRobot 对象,而我们正在其中,导致在 Result := Stream; 行崩溃。 - Ian Boyd
为了完整起见,我将您的想法编写成代码,并将其添加到问题中。 - Ian Boyd
我用代码的副本进行了检查,这里没有问题。在 D2010 中进行了测试。 - Uwe Raabe
那我猜这是 Delphi-5 的一个 bug,我需要另一个解决方案。 :( - Ian Boyd
显示剩余5条评论

3
没有必要让您的委托类继承任何特定的类。只要实现了适当的方法,您就可以继承TObject。我将保持简单,并使用TInterfacedObject进行说明,它提供了您已经确定的3个核心方法。
此外,您不需要 TRobotStream = class(TAggregatedObject, IUnknown, IStream)。相反,您可以简单地声明IStream继承自IUnknown。顺便说一下,我总是为我的接口分配一个GUID(按Ctrl+Shift+G组合键)。
根据您的特定需求,可以应用许多不同的方法和技术。
- 委托给接口类型 - 委托给类类型 - 方法别名
最简单的委派是通过接口。
TRobotStream = class(TinterfacedObject, IStream)

TRobot = class(TInterfacedObject, IStream)
private
  //The delegator delegates the implementations of IStream to the child object.
  //Ensure the child object is created at an appropriate time before it is used.
  FRobotStream: IStream;
  property RobotStream: IStream read FRobotStream implements IStream;
end;

需要注意以下几点:

  • 确保您委托的对象具有适当的生命周期。
  • 确保持有对代理人的引用。请记住,接口是引用计数的,只要计数降至零,就会被销毁。这可能是你头痛的原因

IStream是一个标准接口。可以说它是一个COM流。 - Marco van de Voort
答案缺少创建位于“FRobotStream”内的对象的代码,它从哪里继承,它是如何构造的等信息。 - Ian Boyd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接