Delphi所有权混淆

26

我一直以为所有者(owner)负责销毁视觉控件,如果我将nil作为所有者传递,则可以手动控制销毁。

考虑以下示例:

TMyForm = class (TForm)
private
  FButton : TButton;
end;

...
FButton := TButton.Create(nil);   // no owner!!
FButton.Parent := Self;

我本来期望这个按钮会导致内存泄漏,但实际上没有,而且TButton的析构函数被调用了。

进一步的调查显示,TWinControl的析构函数包含以下代码片段:

I := ControlCount;
while I <> 0 do
begin
  Instance := Controls[I - 1];
  Remove(Instance);
  Instance.Destroy;
  I := ControlCount;
end;

这个问题似乎破坏了子组件(那些具有将Parent设置为该控件本身的组件)。

我没想到父控件会销毁该控件。有人能解释一下为什么会发生这种情况吗?如果我传入一个所有者,是谁在销毁对象?


TComponent.DestroyComponents(从析构函数调用)在您不设置父级而是所有者时进行销毁。我从未注意到销毁也可以发生在TWinControl中,这很好知道。 - Heinrich Ulbricht
2
如何将面板分离并显示在单独的窗口中?这个问题中,存在一些类似的困惑。虽然在TControl.Parent Property的“注释”中有解释,但我觉得其中关于流媒体部分的解释有点令人困惑。 - Sertac Akyuz
2个回答

14

为什么会发生这种情况?

这是合理的并且是按设计实现的。如果父控件被销毁时,你认为孤立的子控件应该如何处理?它们应该突然漂浮在顶层窗口吗?很可能不是。它们应该被重新分配到另一个控件中吗?哪一个?

who is destroying the object if I pass in an owner?
Parent如果被分配并且首先被释放。TWinControl覆盖了TComponent的析构函数,以首先释放其子控件(继承的析构函数稍后才会调用)。子控件通知Owner已被销毁,从其拥有的组件列表中将它们移除。这就是为什么在其析构函数中Owner不会再尝试释放您的对象。
如果ParentOwner是同一个对象,则上述内容也适用。
如果ParentOwner是两个不同的对象,并且您首先释放了Owner,那么Owner组件将释放其所有拥有的组件(请参见TComponent的析构函数)。您的对象是TControl的派生类,TControl覆盖了析构函数以调用SetParent(nil);,该方法从父控件的子控件列表中移除该实例。这就是为什么在其析构函数中父对象不会再尝试释放您的对象。

是的,有一些不是TWinControl后代的所有者,例如数据模块或其他自定义模块。 - Ondrej Kelle
2
哦,我以为我在第一部分已经解释过了。“孤立的子控件应该怎么办?”因为父母关系意味着要负责清理子控件,就像所有权意味着要负责清理所拥有的组件一样。请记住,父对象和所有者对象可能是两个不同的对象。 - Ondrej Kelle
2
我可能忽略了什么,但是我不明白重点在哪里。每个具有“parent”属性的对象也都有一个“owner”属性。那么为什么“parent”应该处理对象的销毁呢?为什么不使用现有的所有权机制来进行销毁呢?“孤立的子控件应该怎么办”:为什么不将其父项设置为“nil”呢? - jpfollenius
1
TOndrej:我并不是说每个已分配父级的对象都有一个所有者!我只是说这样的每个对象都有可能被分配一个所有者,所以我认为没有必要再添加一个销毁机制。 - jpfollenius
1
两个清理职责 -> 两个销毁机制。对我来说,这个需求很明确。 - Ondrej Kelle
显示剩余3条评论

8
我目前可以访问的最早版本是Delphi 5,TWinControl析构函数中也有您发布的代码,因此这种行为已经存在很长时间了。当您考虑它时,它有点说得通 - Controls可视化组件,当您销毁它们的容器(Parent)时,销毁子组件也是有道理的。 TWinComponent的析构函数无法为您决定如何处理它们(隐藏它们?将它们重新分配给Parent.Parent?但是如果当前的Parent是顶级窗口,即它没有Parent怎么办?等等)。因此,VCL的设计者们决定这是最安全的选择,避免内存/句柄泄漏(特别是在早期,其中win句柄非常重要,因此避免泄漏它们可能是最重要的)。因此,如果您希望子元素保留,则应在销毁容器之前重新分配它们的父级。

顺便说一下,如果传递Owner,则TComponent.DestroyComponents;(由TComponent.Destroy调用)会销毁该组件。


如果我传入一个所有者,是什么阻止了Destroy被调用两次?在这种情况下,应该执行两个析构函数... - jpfollenius
从内部列表中删除对子对象的引用,因此第二个析构函数将不会“看到”(已经被销毁的)子对象。 - ain
但是:TComponent 没有使用 Controls 数组(包含子对象),而是使用 Components 列表,控件不应该从中删除。 - jpfollenius
2
VCL 中有一个通知系统,必须处理所有不同的情况,以便及时更新内部所有权列表。即 TComponent 的析构函数调用 if FOwner <> nil then FOwner.RemoveComponent(Self); - ain
ain:你能详细解释一下吗(也许在回答中进行编辑)?我看不到TWinControl的析构函数从其所有者的组件列表中删除控件... - jpfollenius
@Smasher 由于所有控件都是从TComponent继承而来的,它们在析构函数中(如果它们有所有者)都会调用所有者的TComponent.RemoveComponent()方法,然后在其中调用Remove(AComponent);方法将组件从其所有者列表中移除。这个回答解决了你的问题吗? - ain

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