Delphi编译器何时创建隐式接口变量?

4
我在StackOverflow上测试了一段代码,并遇到了一个情况,隐式接口变量出现了。但我看不出这种情况的原因。我有一个迷你工厂,返回一个新创建对象的接口。如果我在过程块中调用该方法,则只得到1个引用计数。如果我在主程序块中调用它,则会得到2个引用计数。
我使用Delphi 10 Seattle进行了测试。是否有资源包含隐式接口创建的规则,以及通过工厂返回接口的模式是否可靠?
program TestRefCount;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  Vcl.Dialogs;

type
  IMyInterface = interface(IInterface)
    ['{62EB2C46-9B8A-47CE-A881-DB96E6F6437D}']
    procedure DoSomething;
    function GetRefCount: Integer;
  end;

  TMyObject = class(TInterfacedObject, IMyInterface)
  strict private
    FMyValue: Integer;
  public
    procedure Init;
    procedure DoSomething;
    function GetRefCount: Integer;
  end;

  TMyFactory = class(TObject)
  private
    function CreateMyInt: IMyInterface;
  end;

procedure TMyObject.DoSomething;
begin
  MessageDlg(IntToStr(FMyValue), mtInformation, [mbok], 0);
end;

function TMyObject.GetRefCount: Integer;
begin
  Result := FRefCount;
end;

procedure TMyObject.Init;
begin
  FMyValue := 100;
end;

function TMyFactory.CreateMyInt: IMyInterface;
var
  myObject: TMyObject;
begin
  myObject := TMyObject.Create;
  Assert(myObject.GetRefCount = 0);
  myObject.Init;
  Assert(myObject.GetRefCount = 0);
  Result := myObject;
  Assert(myObject.GetRefCount = 1);
  Assert(Result.GetRefCount = 1);
end;

procedure WorkWithIntf;
var
  myFactory: TMyFactory;
  myInt: IMyInterface;
begin
  myFactory := TMyFactory.Create;
  try
    myInt := myFactory.CreateMyInt;
    Assert(myInt.GetRefCount = 1);
    myInt.DoSomething;
    Assert(myInt.GetRefCount = 1);
  finally
    myFactory.Free;
  end;
end;

var
  myFactory: TMyFactory;
  myInt: IMyInterface;
begin
  try
    // This case doesn't have an implicit interface variable
    WorkWithIntf;
    // This case does have an implicit interface variable
    myFactory := TMyFactory.Create;
    try
      myInt := myFactory.CreateMyInt;
      Assert(myInt.GetRefCount = 1); // This fails because the refcount is 2
      myInt.DoSomething;
      Assert(myInt.GetRefCount = 1);
    finally
      myFactory.Free;
    end;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

这是第一个块,其中没有隐式接口变量:

TestRefCount.dpr.67: myInt := myFactory.CreateMyInt;
005C6A5A 8D55F8           lea edx,[ebp-$08]
005C6A5D 8B45FC           mov eax,[ebp-$04]
005C6A60 E83BFEFFFF       call TMyFactory.CreateMyInt
TestRefCount.dpr.68: Assert(myInt.GetRefCount = 1);
005C6A65 8B45F8           mov eax,[ebp-$08]

这里是第二个模块,我们可以看到隐式接口变量:
TestRefCount.dpr.86: myInt := myFactory.CreateMyInt;
005CF513 8D55EC           lea edx,[ebp-$14]
005CF516 A19CB75D00       mov eax,[$005db79c]
005CF51B E88073FFFF       call TMyFactory.CreateMyInt
005CF520 8B55EC           mov edx,[ebp-$14]
005CF523 B8A0B75D00       mov eax,$005db7a0
005CF528 E8C7E3E3FF       call @IntfCopy
TestRefCount.dpr.87: Assert(myInt.GetRefCount = 1); // This fails because the refcount is 2
005CF52D A1A0B75D00       mov eax,[$005db7a0]
1个回答

0

据我理解,虽然没有任何文档记录,编译器对待局部变量和全局变量的方式是不同的。对于局部变量,编译器相信分配给局部变量的赋值将成功。因此,不需要隐式局部变量。这是第一种情况。

对于全局变量,编译器更加谨慎。它对全局变量持怀疑态度。为了防止变量赋值失败,它会发出防御性代码。编译器首先分配一个隐式局部变量,这个分配肯定会成功。因此,接口的引用计数会增加。然后再分配给全局变量。如果分配失败,那么至少隐式局部变量有一个引用,并且能够递减并正确释放接口。

你可能会想知道编译器为什么紧张地分配给全局变量。你知道它是安全的,不可能失败,编译器害怕什么?它对全局变量的概念更广泛。例如,它认为指向接口引用的指针是全局的。编译器试图防止该指针无效、分配失败,以及没有人引用接口。编译器只考虑两种情况:局部和全局。它相信对局部变量的赋值,而将其他所有内容都归为潜在风险的全局变量,包括你完全安全的全局变量。

在我看来,编译器过于谨慎了。如果程序员说变量可以被赋值,我认为编译器不应该怀疑它。如果程序员犯了错误,那么程序员应该准备接受后果。无论是泄漏还是运行时内存访问失败。但设计者采取了不同的、更保守的方法。
另一个场景是当你在函数返回值上使用as运算符时,你会看到一个隐式的局部变量。例如:
Foo := GetBar as IFoo;

关于这个问题的更多信息请参见:意外隐式接口变量的神秘案例

虽然那个案例很明显。但是隐式本地变量是必不可少的,因为as引发异常是完全合理的。


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