所以,我的父级(即“控制器”)对象声明它实现了 `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
。
当我在下面代码结束后询问TRobotStream
的IStream
时:
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);
var rs: IStream; rs := TRobot.Create;
在创建TRobotStream
时,引用计数返回到零,销毁了自身的TRobot
对象,而我们正在其中,导致在Result := Stream;
行崩溃。 - Ian Boyd