在 Delphi 中管理对象生命周期





5.00/5 (3投票s)
在本文中,我将简要回顾 Delphi 应用程序中管理对象生命周期的方法。
引言
Delphi 语言提供了一套不同的对象生命周期管理方法。我想描述它们及其优缺点。在实际项目中,你会发现不同的方法被用在各种地方,因此理解何时一种方法优于另一种方法很重要。
简而言之,Delphi 中管理对象生命周期的方法有:
- 手动调用对象析构函数
- 基于作用域的自动管理
- 基于所有者的自动管理
- 基于引用计数的自动管理
- 基于垃圾回收的自动管理
Delphi 本身不支持垃圾回收,尽管可以手动实现类似垃圾回收的东西,但我不会在本文中涵盖这个主题(在我看来,这是一个单独文章的主题)。我将在后面详细介绍其他指定的方法。
手动调用对象析构函数
手动调用对象析构函数意味着程序员将显式控制对象的创建和销毁时间。让我们看一个例子。
type
TMyObject = class
public
// Конструктор.
constructor Create;
{ TObject }
destructor Destroy; override;
end;
...
var
MyObject: TMyObject;
begin
MyObject := TMyObject.Create;
try
...
finally
MyObject.Free;
end;
end;
手动调用对象构造函数和析构函数为程序员提供了对情况的最大控制。这种方法非常适合在某个方法内创建并在那里销毁的对象。对于生命周期与应用程序生命周期匹配的对象(它们通常在单元初始化节创建,在单元最终化节销毁),以及对于在某个对象构造函数中创建并在同一对象析构函数中销毁、并且实例引用不返回到对象外部的对象,这种方法也同样适用。
基于作用域的自动管理
在 Delphi 中,有一些对象类型不需要你手动创建或销毁。它们是基本类型(如 Integer、Boolean、Double)和记录类型。当这些类型的对象用作局部变量时,它们被放置在栈上。这意味着这些对象的内存是自动分配和释放的。Delphi 中的类成员总是放置在堆上,你无法将类数据放置在栈上。但是,基本类型和记录类型的类成员不需要显式分配和释放,所有这些都是自动完成的。
基于所有者的自动管理
在管理对象生命周期时,存在一种常见情况。那就是当对象被分组到一个树中,并且所有树节点都必须与树根同时销毁。例如,所有窗体控件都必须与窗体本身一起销毁。如果程序员编写手动销毁窗体控件的代码,这段代码将会是统一且冗余的。因此,Delphi 允许创建派生自 `TComponent` 类的组件,其生命周期是基于所有者管理的。这意味着所有组件都组织成一棵树,树中的每个组件(除了根节点)都有一个所有者。每个所有者组件在其 `Components` 列表中包含所有子组件。当根组件被销毁时,所有子组件也会自动销毁。通常,所有者组件在调用具有 `Owner` 参数的对象构造函数时指定。但之后你可以使用 `RemoveComponent` / `InsertComponent` 方法更改组件的所有者。
type
TComponent = class(TPersistent)
public
constructor Create(AOwner: TComponent); virtual;
...
procedure InsertComponent(const AComponent: TComponent);
procedure RemoveComponent(const AComponent: TComponent);
...
property Components[Index: Integer]: TComponent read GetComponent;
property ComponentCount: Integer read GetComponentCount;
property ComponentIndex: Integer read GetComponentIndex write SetComponentIndex;
...
property Owner: TComponent read FOwner;
...
end;
下一个代码示例演示了在销毁父组件时销毁整个对象树的可能性。
type
TParentComponent = class(TComponent)
...
public
{ TObject }
destructor Destroy; override;
end;
TChildComponent = class(TComponent)
...
public
{ TObject }
destructor Destroy; override;
end;
{ TParentComponent }
destructor TParentComponent.Destroy;
begin
ShowMessage('TParentComponent.Destroy');
inherited;
end;
{ TChildComponent }
destructor TChildComponent.Destroy;
begin
ShowMessage('TChildComponent.Destroy');
inherited;
end;
...
var
ParentComponent: TParentComponent;
ChildComponent: TChildComponent;
begin
ParentComponent := TParentComponent.Create(nil);
try
ChildComponent := TChildComponent.Create(ParentComponent);
...
finally
ParentComponent.Free;
end;
end;
当调用 `ParentComponent` 析构函数时,也会调用 `ChildComponent` 析构函数。
Delphi 的优势在于能够以 DFM 格式序列化和反序列化这样的对象树。但我认为这属于另一篇文章的讨论范畴。
基于引用计数的自动管理
如果对象被许多其他对象使用,并且没有任何一个对象可以作为所有者,那么基于引用计数的对象生命周期管理方法就派上用场了。这种方法变得有用的一个典型情况是,当你有一个方法,它将创建对象的引用返回到方法外部。你无法保证客户端代码会调用对象析构函数。在这种情况下,返回一个生命周期由引用计数管理的接口引用会更方便。
Delphi 原生支持与接口协同工作。接口声明包含接口属性和方法。拥有接口引用,我们可以透明地访问这些属性和方法。然而,与类不同,接口本身不包含任何成员数据。成员数据是实现该接口的类的部分。使用接口进行编程提供了很大的灵活性,因为我们不被绑定到特定的接口实现,并且可以在需要时随时更改它们。我应该说,使用接口本身并不意味着我们将使用引用计数。但是借助 Delphi 编译器的魔力,我们可以轻松地为实现某个接口的对象实现引用计数。
每当创建接口引用时,Delphi 编译器都会调用接口的 `_AddRef` 方法。当接口引用被分配给局部变量或类成员时,当引用接口的参数传递到某个方法时,或者当使用类型转换获取接口引用时,都会发生这种情况。每当接口引用超出作用域时,Delphi 编译器都会调用接口的 `_Release` 方法。特别是,当带有接口引用的局部变量超出作用域时,带有接口引用参数的函数执行完成时,或者带有接口引用成员的类的析构函数被调用时,都会调用此方法。`_AddRef` 和 `_Release` 方法是基本接口 `IInterface` 的一部分。所有 Delphi 接口都继承自 `IInterface`,因此任何实现接口的类都具有这些方法。
`_AddRef` 和 `_Release` 方法的实现是实现该接口的类的责任。实现引用计数的代码相当简单。在每次 `_AddRef` 调用中增加内部引用计数器,在每次 `_Release` 调用中减少计数器就足够了。当引用计数器变为零时,应调用对象析构函数。幸运的是,在大多数情况下,我们不需要手动实现 `_AddRef` 和 `_Release` 方法。在 Delphi 中,我们可以继承我们的对象自 `TInterfacedObject`,它具有这些方法的默认实现。代码示例如下:
type
IMyInterface = interface
['{2A307AD1-7465-4EA7-9245-1A87CE42F931}']
procedure DoSomething;
end;
TMyClass = class(TInterfacedObject, IMyInterface)
private
{ IMyInterface }
procedure DoSomething;
end;
...
var
MyIntf, MyIntf2: IMyInterface;
begin
MyIntf := TMyClass.Create as IMyInterface;
MyIntf2 := MyIntf;
try
MyIntf.DoSomething;
finally
MyIntf := nil;
MyIntf2 := nil;
end;
end.
在示例中,当最后一个接口引用被清除时,会调用对象析构函数。
可以替换继承的 `_AddRef` 和 `_Release` 方法的实现,用你自己的代码。例如,当需要使用接口但不希望引用它们时,就需要这样做。请看下一个示例,其中 `_AddRef` 和 `_Release` 方法的实现已被替换,并且没有进行引用计数:
type
IMyInterface = interface
['{2A307AD1-7465-4EA7-9245-1A87CE42F931}']
procedure DoSomething;
end;
TMyClass = class(TInterfacedObject, IMyInterface)
private
{ IInterface }
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
{ IMyInterface }
procedure DoSomething;
public
constructor Create;
{ TObject }
destructor Destroy; override;
end;
{ TMyClass }
constructor TMyClass.Create;
begin
inherited;
WriteLn('TMyClass.Create');
end;
destructor TMyClass.Destroy;
begin
WriteLn('TMyClass.Destroy');
inherited;
end;
procedure TMyClass.DoSomething;
begin
WriteLn('TMyClass.DoSomething');
end;
function TMyClass._AddRef: Integer;
begin
WriteLn('TMyClass._AddRef');
Result := 0;
end;
function TMyClass._Release: Integer;
begin
WriteLn('TMyClass._Release');
Result := 0;
end;
...
var
MyClass: TMyClass;
MyIntf: IMyInterface;
begin
MyClass := TMyClass.Create;
MyIntf := MyClass as IMyInterface;
try
MyIntf.DoSomething;
finally
MyIntf := nil;
MyClass.Free;
end;
end.
当对象继承自 `TComponent` 时,它可以实现接口,但它不是引用计数的。这是因为对于组件来说,使用基于所有者的对象生命周期管理是更可取的。