Delphi 当直接将对象实例作为常量接口参数传递时,编译器是否应该提示/警告?

Delphi 当直接将对象实例作为常量接口参数传递时,编译器是否应该提示/警告?,delphi,interface,parameters,constants,Delphi,Interface,Parameters,Constants,当将对象的新实例传递给具有对象类实现的接口的const interface参数的方法时,编译器是否应该提示/警告 编辑:当然,示例很简单,可以说明这个问题。但在现实生活中,它变得更加复杂:如果创建和使用的代码相距很远(不同的单元、不同的类、不同的项目),该怎么办?如果它是由不同的人维护的呢?如果一个非常量参数变成常量参数,并且不是所有调用代码都可以检查(因为更改代码的人没有访问所有调用代码的权限),该怎么办 下面这样的代码崩溃了,很难找到原因 首先是日志: 1.Run begin 1.RunL

当将对象的新实例传递给具有对象类实现的接口的const interface参数的方法时,编译器是否应该提示/警告

编辑:当然,示例很简单,可以说明这个问题。但在现实生活中,它变得更加复杂:如果创建和使用的代码相距很远(不同的单元、不同的类、不同的项目),该怎么办?如果它是由不同的人维护的呢?如果一个非常量参数变成常量参数,并且不是所有调用代码都可以检查(因为更改代码的人没有访问所有调用代码的权限),该怎么办

下面这样的代码崩溃了,很难找到原因

首先是日志:

1.Run begin

1.RunLeakCrash
 2.RunLeakCrash begin
     NewInstance 1
     AfterConstruction 0
   3.LeakCrash begin
     _AddRef 1
    4.Dump begin
    4.Dump Reference=10394576
    4.Dump end
     _Release 0
     _Release Destroy
     BeforeDestruction 0
   3.LeakCrash Reference got destroyed if it had a RefCount of 1 upon entry, so now it can be unsafe to access it
     _AddRef 1
    4.Dump begin
    4.Dump Reference=10394576
    4.Dump end
     _Release 0
     _Release Destroy
     BeforeDestruction 0
   3.LeakCrash end with exception

1.Run end
EInvalidPointer: Invalid pointer operation
然后,过早释放实现接口的对象实例的代码:

//{$define all}

program InterfaceConstParmetersAndPrematureFreeingProject;

{$APPTYPE CONSOLE}

uses
  SysUtils,
  Windows,
  MyInterfacedObjectUnit in '..\src\MyInterfacedObjectUnit.pas';

procedure Dump(Reference: IInterface);
begin
  Writeln('    4.Dump begin');
  Writeln('    4.Dump Reference=', Integer(PChar(Reference)));
  Writeln('    4.Dump end');
end;

procedure LeakCrash(const Reference: IInterface);
begin
  Writeln('   3.LeakCrash begin');
  try
    Dump(Reference); // now we leak because the caller does not keep a reference to us
    Writeln('   3.LeakCrash Reference got destroyed if it had a RefCount of 1 upon entry, so now it can be unsafe to access it');
    Dump(Reference); // we might crash here
  except
    begin
      Writeln('   3.LeakCrash end with exception');
      raise;
    end;
  end;
  Writeln('   3.LeakCrash end');
end;

procedure RunLeakCrash;
begin
  Writeln(' 2.RunLeakCrash begin');
  LeakCrash(TMyInterfacedObject.Create());
  Writeln(' 2.RunLeakCrash end');
end;

procedure Run();
begin
  try
    Writeln('1.Run begin');

    Writeln('');
    Writeln('1.RunLeakCrash');
    RunLeakCrash();

  finally
    Writeln('');
    Writeln('1.Run end');
  end;
end;

begin
  try
    Run();
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  Readln;
end.
EInvalidPointer将在对
Dump(Reference)的第二次调用中显示自己。
原因是公开引用的基础对象的引用计数已为零,因此基础对象已被销毁

关于编译器插入或省略的引用计数代码的几点注意事项:

  • 未标记为
    常量的参数(如
    过程转储(引用:IInterface);
    )获取隐式try/finally块以执行引用计数
  • 标有
    const
    (如
    过程泄漏崩溃(const Reference:IInterface);
    )的参数不会获得任何引用计数代码
  • 传递对象实例创建的结果(如
    LeakCrash(TMyInterfacedObject.Create());
    )不会生成任何引用计数代码
以上所有编译器行为都是非常符合逻辑的,但它们结合起来可能会导致EInvalidPointer。
EInvalidPointer仅以非常狭窄的使用模式显示自己。
编译器很容易识别该模式,但当您陷入其中时,很难调试或找到原因。
解决方法非常简单:将
TMyInterfacedObject.Create()
的结果缓存在中间变量中,然后将其传递给
LeakCrash()

编译器是否应该提示或警告您此使用模式

最后,我用来跟踪所有_AddRef/_Release/etcetera调用的代码:

unit MyInterfacedObjectUnit;

interface

type
  // Adpoted copy of TInterfacedObject for debugging
  TMyInterfacedObject = class(TObject, IInterface)
  protected
    FRefCount: Integer;
    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
  public
    procedure AfterConstruction; override;
    procedure BeforeDestruction; override;
    class function NewInstance: TObject; override;
    property RefCount: Integer read FRefCount;
  end;

implementation

uses
  Windows;

procedure TMyInterfacedObject.AfterConstruction;
begin
  InterlockedDecrement(FRefCount);
  Writeln('     AfterConstruction ', FRefCount);
end;

procedure TMyInterfacedObject.BeforeDestruction;
begin
  Writeln('     BeforeDestruction ', FRefCount);
  if RefCount <> 0 then
    System.Error(reInvalidPtr);
end;

class function TMyInterfacedObject.NewInstance: TObject;
begin
  Result := inherited NewInstance;
  TMyInterfacedObject(Result).FRefCount := 1;
  Writeln('     NewInstance ', TMyInterfacedObject(Result).FRefCount);
end;

function TMyInterfacedObject.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
  Writeln('     QueryInterface ', FRefCount);
  if GetInterface(IID, Obj) then
    Result := 0
  else
    Result := E_NOINTERFACE;
end;

function TMyInterfacedObject._AddRef: Integer;
begin
  Result := InterlockedIncrement(FRefCount);
  Writeln('     _AddRef ', FRefCount);
end;

function TMyInterfacedObject._Release: Integer;
begin
  Result := InterlockedDecrement(FRefCount);
  Writeln('     _Release ', FRefCount);
  if Result = 0 then
  begin
    Writeln('     _Release Destroy');
    Destroy;
  end;
end;

end.
单位MyInterfacedObjectUnit;
接口
类型
//用于调试的TInterfacedObject的Adpoted副本
TMyInterfacedObject=class(TObject,IInterface)
受保护的
FRefCount:整数;
函数查询接口(const IID:TGUID;out Obj):HResult;stdcall;
函数_AddRef:Integer;stdcall;
函数释放:整数;stdcall;
公众的
施工后的程序;推翻
销毁前的程序;推翻
类函数NewInstance:TObject;推翻
属性RefCount:整数读取FRefCount;
结束;
实施
使用
窗户;
程序TMyInterfacedObject.AfterConstruction;
开始
联锁减量(FRefCount);
Writeln('AfterConstruction',FRefCount);
结束;
销毁前的TMI接口对象程序;
开始
Writeln(‘销毁前’,FRefCount);
如果RefCount为0,则
系统错误(reInvalidPtr);
结束;
类函数TMyInterfacedObject.NewInstance:ToObject;
开始
结果:=继承的NewInstance;
TMyInterfacedObject(结果)。FRefCount:=1;
Writeln('NewInstance',TMyInterfacedObject(Result).FRefCount);
结束;
函数TMyInterfacedObject.QueryInterface(const IID:TGUID;out Obj):HResult;
开始
Writeln('QueryInterface',FRefCount);
如果获取接口(IID,Obj),则
结果:=0
其他的
结果:=E_NOINTERFACE;
结束;
函数TMyInterfacedObject.\u AddRef:Integer;
开始
结果:=联锁增量(FRefCount);
Writeln('u AddRef',FRefCount);
结束;
函数TMyInterfacedObject.\u Release:Integer;
开始
结果:=联锁减量(FRefCount);
Writeln('u Release',FRefCount);
如果结果=0,则
开始
Writeln('u Release Destroy');
破坏;
结束;
结束;
结束。

--jeroen

我投票赞成一个警告,因为即使是有经验的开发人员也可能落入这个陷阱。如果任何人不想看到这个警告,它很容易被禁用,这意味着当前的行为没有改变。这有点像对未初始化变量的警告


这个问题的另一个解决方案是隐式的
Assert(InterfaceParameter.RefCount>0)用于常量接口参数。可能仅在启用断言时发出。

传递对象实例创建的结果(如LeakCrash(TMyInterfacedObject.Create());)不会生成任何引用计数代码


上面是编译器错误。当程序存在时,它必须创建一个隐藏的var并减少计数器

这是一个bug。RunLeakCrash中从实例到接口引用的转换应该是一个临时变量,使它在RunLeakCrash期间保持活动状态。

我已经为此工作了10年了。我不敢相信这个问题还不为人所知,所以我认为它是设计出来的/不会解决的。今天回想起来,很明显它是可以修复的,因为它不适用于其他托管类型(字符串、dyn数组、变体等)。@Jeroen@Barry快速搜索QC表明,正如我所怀疑的,这个问题是众所周知的。我找到了以下所有关于这一问题的票:#31164、#71015、#75036、#90025。我相当肯定还有更多#31164被皮埃尔·勒里奇(Pierre le Riche)解析为“按设计”,他评论道:“编译器不可能总是保护程序员不受其影响。当将对象和接口引用混合到同一对象时,您必须小心避免此类问题。”@Jeroen@Barry很明显,在QC上就这个问题进行报告和投票是无法完成的。如果巴里能从里面做点什么的话