如何检查对象引用是否仍然有效?

5

我有一个问题,我正在尝试确定一个对象的引用是否有效。但它似乎返回奇怪的结果。

procedure TForm1.Button1Click(Sender: TObject);
  var form1 : TForm;
      ref2 : TControl;
begin
  form1 := TForm.Create(nil);
  form1.Name := 'CustomForm';
  form1.Parent := self; //Main Form

  form1.Show;

  ref2 := form1;
  showmessage(ref2.ClassName+' - '+ref2.Name+' - '+BoolToStr(ref2.visible,true));
  freeandnil(form1);
  showmessage(ref2.ClassName+' - '+ref2.Name+' - '+BoolToStr(ref2.visible,true));
end;

第一个showmessage返回值为“TForm - CustomForm - True”(就像我所期望的那样)。
第二个showmessage返回值为“TForm - - False”。实际上,我希望能够出现某种访问冲突,然后捕获它并知道该引用无效。
在我的应用程序中,我需要编译一个随机TForm子类列表,因为它们被创建,然后稍后检查它们是否已经消失(或不可见)。不幸的是,这是一个基于插件的系统,所以我不能去改变所有这些表单来发布“我完成了消息”。
像这样的代码是否安全使用(假设我确实正在检查访问违规)?有人有任何想法发生了什么。
谢谢。

请参考之前有关在调用FreeAndNil后使用对象的问题。https://dev59.com/HUXRa4cB1Zd3GeqPwf8n - Rob Kennedy
2
尽管您无法更改所有表单以告诉您何时完成(因为插件的代码在您的控制范围之外),但是您可以要求任何合规的插件必须发送此类消息。 - Rob Kennedy
记录一下,我完全意识到这不是一个理想的解决方案。理想情况下,每个单独的插件都应该发布一些内容,或者更好的方法是解决最初需要这样做的架构问题。不幸的是,改变所有现有插件以符合新标准并不切实际。 - tmjac2
9个回答

7
问题在于,有一定的可能性被访问的内存仍然由Delphi内存管理器保留。在这种情况下,Windows不会生成任何类型的访问冲突,因为该内存属于您!一种可能性是切换到不同的Delphi内存管理器,它可以检测已释放对象的使用。例如,FastMM4具有多个“内存卫生”检查,非常有用于调试,但即使如此,您也无法立即捕获所有这些错误。您可以从SourceForge 下载FastMM4

4
可能性很高,但并非100%确定。通常的解决方案是制定一些计划或系统来确定谁负责释放谁,并不随意共享参考资料。 - Marco van de Voort
我决定用另一种方式实现,不需要找出引用是否有效。上述代码实际上是可以工作的,但我不想依赖于可能会在以后改变的内存黑客/错误/不一致性。谢谢。 - tmjac2
我很好奇你是否尝试了Deltic的建议,只是为了看看是否会收到通知。它应该可以工作,但如果你感到满意,那也没关系。 - Warren P

7
任何TComponent(例如TForm派生类)都可以注册通知其他组件被销毁的事件。
在您的表单中,为每个希望被通知销毁的表单调用FreeNotification(form)。然后在同一表单中覆盖Notification()方法。当任何已调用FreeNotification()的表单(或其他组件)被销毁时,您的Notification()方法将被调用,并带有一个Component参数引用该表单和一个opRemove操作。
如果我正确理解您想要实现的目标,那么这些信息应该足够让您设计出所需的方法。

这是一个很好的方法。我遇到了类似的情况,这个方法非常有效。奇怪的是,在你提出这个方法之前,我从未见过它。谢谢。 - tmjac2

2

之后

freeandnil(form1);

Delphi 内存管理器只是将 Form1 分配的内存标记为自由,但是 Form1 的所有数据仍然存在,并且可以通过 Ref2 访问,直到内存管理器重新使用释放的内存分配给其他对象。
你无法通过这种方式检查 Ref2 是否引用有效对象。像这样的代码是不安全的,实际上是一个错误。 如果您想获得100%的访问冲突,请按以下方式修改代码(如果 Form1 被释放,则 ref2^ = nil):
procedure TForm1.Button1Click(Sender: TObject);
  var form1 : TForm;
      ref2 : ^TControl;
begin
  form1 := TForm.Create(nil);
  form1.Name := 'CustomForm';
  form1.Parent := self; //Main Form

  form1.Show;

  ref2 := @form1;
  showmessage(ref2^.ClassName+' - '+ref2^.Name+' - '+BoolToStr(ref2^.visible,true));
  freeandnil(form1);
  showmessage(ref2^.ClassName+' - '+ref2^.Name+' - '+BoolToStr(ref2^.visible,true));
end;

2
没有可靠的方法使用你尝试的技术来做你想做的事情。已经“消失”的表单可能会重用它们的内存,甚至可能为新表单重用。最好的办法是构建某种机制,通过缓存Screen.Forms迭代的结果,但你仍然可能遇到意外的重复项,其中一个表单被销毁并重新分配,并获得相同的对象地址。然而,这种情况比内存被重用为其他对象的情况更少见。

1
在类似的情况下,我使用一个单例对象来保存所有已创建表单的列表。每个表单都有一个字段,其中包含对此对象的引用。
TMyForm = class(TForm)
private
  //*** This is the reference to the singleton...
  FFormHandler: TFormHandler;
public
  ...
  //*** you might want to publish it as a property:
  property FormHandler: TFormHandler read FFormHandler write FFormHandler;
end;

您可以在调用构造函数时设置此引用:

TMyForm.Create(aFormHandler: TFormHandler; aOwner: TComponent)
begin
  FFormHandler := aFormHandler;
  inherited Create(aOwner);
end;

(或者,如果您不想更改构造函数的参数,您可以在创建表单后直接从外部设置字段)。

当表单被销毁时,它会通知处理程序并告诉他从列表中删除该表单 - 大致如此:

TMyForm.Destroy(Sender: TObject);
begin
  FFormHandler.RemoveFromFormList(Self);
  inherited;
end;

(跟踪细节不包括在示例中 - 例如需要一个“AddToFomList”或类似的方法)

喜欢这个想法,但并没有很好地回答问题。我最终会做类似的事情,只是用一个不同的事件替换OnDestroy事件(然后调用原始事件),以便我可以保持活动窗体的列表。 - tmjac2

1

0

这很简单,只需与NIL进行比较:

    // object declaration

Type object;

object = new Type();

...


// here you want to be sure of the existance of the object:

if (object <> nil )

  object.free;

不行,没有机会。代码 MyObject.Free 释放了对象,但并没有将 MyObject 设置为 nil。要做到这一点,可以使用 FreeAndNil。然而,这并没有考虑到对同一对象有多个引用的情况。 - tmjac2

0

考虑到您无法修改插件中的代码,所有关于如何编写更安全的代码的好方法都不适用于您的情况。

  • 您可以通过检查对象引用是否仍然符合预期来执行一种方法,方法是查找VMT。这个想法最初由Ray Lischner(他提倡使用FreeAndNil)和Hallvard Vassbotn后来发表:请参见this SO answer

  • 另一种更好的方法是使用FastMM4 FullDebugmode,它会在释放内存到可用池之前将所有已释放的对象替换为TFreeObject实例,但会引入主要减速。

请注意,这两种方法都无法防止在同一内存地址创建另一个相同类的实例时出现误报。您将获得正确类型的有效对象,但不是原始对象。(在您的情况下不太可能,但有可能发生)

-1
如果您无法以其他方式进行测试,您可以将此作为最后的手段。
function IsValidClass( Cls: TClass ): Boolean;
var
    i: Integer;
begin
    for i := 0 to 99 do begin
        Result := ( Cls = TObject ); // note that other modules may have a different root TObject!
        if Result then Exit;
        if IsBadReadPtr( Cls, sizeof( Pointer ) ) then Break;
        if IsBadReadPtr( Pointer( Integer( Cls ) + vmtParent ), sizeof( Pointer ) ) then Break;
        Cls := Cls.ClassParent;
    end;
    Result := False;
end;

function IsValidObject( Obj: TObject ): Boolean;
begin
    Result := not IsBadReadPtr( Obj, sizeof( Pointer ) ) and IsValidClass( Obj.ClassType ) and not IsBadReadPtr( Obj, Obj.InstanceSize );
end;

IsBadReadPtr 函数来自于 Windows。


这仅仅决定了内存内容是否看起来像一个有效的对象。即使是新释放的对象看起来仍然是有效的,但实际上并不是。如果被释放的内存被重新分配给一个新对象,那么对旧对象的悬空引用将会突然变得有效,尽管它们指向的不是你认为的东西。除此之外,有很多信息描述了不使用IsBadReadPtr的原因。如果你的程序不知道自己的指针是否有效,那么你已经输了游戏。 - Rob Kennedy
@Rob:是的,你说得对。我忘了提到我在调试模式下与FastMM一起使用它。我在我们的异常处理程序中使用此代码,在终止应用程序之前尝试将尽可能多的信息保存到日志文件中。我同意你的看法,这只是用于调试而不是常规使用。 - Ritsaert Hornstra

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