Introduction
Delphi language gives a set of different approaches to manage object lifetime. I would like to desribe them and their pro and cons. In real project one will actually find different approaches used here and there, so it is important to understand when one approach is better then the other.
In short there are such approaches in managing object lifetime in Delphi:
- manual calling object destructor
- automatic managing based on scope
- automatic managing based on owner
- automatic managing based on reference counting
- automatic managing based on garbage collection
Delphi doesn't support natively garbage collection, though it is possible to implement something like garbage collector manually, but I won't cover this topic in the article (in my opinion it is the topic for separate article). I will desribe other specified approaches in details further.
Manual calling object destructor
Manual calling object destructor means that programmer would explicitly control when object will be created and when object will be destroyed. Let's see the example:
type
TMyObject = class
public
constructor Create;
destructor Destroy; override;
end;
...
var
MyObject: TMyObject;
begin
MyObject := TMyObject.Create;
try
...
finally
MyObject.Free;
end;
end;
Manual calling object contructor and destructor gives programmer maximal control over the situation. Such an approach is suited well for objects, which are created inside some method and destroyed there too. Such an approach is suited well also for objects, which lifetime matches application lifetime (they are usually created in unit initialization section and destroyed in unit finalization section). And also this approach is suited well for objects, which are created in some object constructor and destroyed in the same object destructor, when the references to the instance are not returned outside of the object.
Automatic managing based on scope
In Delphi there are some kinds of objects, which you don't need to create or destroy manually. They are the objects of primitive types, such as Integer, Boolean, Double, and record types as well. These types of objects are placed on the stack, when they are used as local variables. This means that memory for such objects is allocated and freed automatically. Class members in Delphi are always placed in the heap, you cannot place class data on the stack. But class members of primitive types and records don't need explicit allocation and deallocation, this all is made automatically.
Automatic managing based on owner
There is one frequent situation in managing object lifetime. It is the one when objects are grouped into some tree, and all tree nodes are to be destroyed in the same time as the tree root. For example, all form controls are to be destroyed together with the form itself. If programmer was to write the code of manual destroying form controls, this code would be uniform and redundant. So Delphi makes it possible to create components derived from the class TComponent
, which lifetime is managed based on owner. It means that all components are organized into tree so that each component in the tree excluding root has an owner. Each owner component on the other side contains all child components in the Components
list. When root component is being destroyed all child components are destroyed automatically as well. Usually the owner of component is specified when calling object constructor (which has Owner
argument). But you can change the owner of the component after that using RemoveComponent
/InsertComponent
methods.
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;
The next code example demonstrates the posiibility to destroy a whole object tree when destroying parent component:
type
TParentComponent = class(TComponent)
...
public
destructor Destroy; override;
end;
TChildComponent = class(TComponent)
...
public
destructor Destroy; override;
end;
destructor TParentComponent.Destroy;
begin
ShowMessage('TParentComponent.Destroy');
inherited;
end;
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;
When ParentComponent
destructor is called ChildComponent
destructor is called as well.
The advantage of Delphi is the possibility to serialize and deserialize such object tree in DFM format. But I think that this is a matter for other article.
Automatic managing based on reference counting
If the object is being used by many other objects and no one of them can be object owner the method of managing object lifetime based on reference counting comes into place. One typical situation when this method becomes useful is when you have some method, which returns the reference to created object outside of the method. You cannot guarantee that client code will call object destructor. In such situation it is much more handy to return interface reference, which lifetime is managed based on reference counting.
Delphi natively supports working with interfaces. Interface declarations contains interface properties and methods. Having interface reference we can transparently access these properties and methods. However in contrast to class interface does not contain any member's data. Members's data are the part of class which implements this interface. Programming with interfaces gives a lot of flexibility, because we are not bound to particular intrerface implementations and can change them any time when we need it. I should say that using interfaces doesn't mean itself that we will use reference counting. But with Delphi compiler magic we can easy implement reference counting for the object, which implements some interface.
Every time when interface reference is created Delphi compiler calls _AddRef
method of the interface. This happens when interface reference is assigned to local variable or class member, when reference to interface is passed as argument into some method or when typecast is used to gain interface reference. Any time when interface reference goes out of scope Delphi compiler calls _Release
interface method. In particular this method is called when local variable with interface reference goes out of scope, when function with interface reference argument execution is finished or when class with interface reference members destructor is called. Methods _AddRef
and _Release
are the part of the base interface IInterface
. ALl Delphi interfaces are inherited from IInterface
, that's why any class implementing interface has these methods.
Implementation of methods _AddRef
and _Release
methods is responsibility of class, which implements interface. Implementation, which does reference counting is pretty simple. It is enough to increment internal reference counter inside each _AddRef
call and decrement the counter inside each _Release
call. When reference counter becomes zero object destructor is to be called. Fortunately, in majority of situations we don't need to implement _AddRef
and _Release
nethods manually. In Delphi we can inherit our objects from TInterfacedObject
, which has default implementation of these methods. The code sample follows:
type
IMyInterface = interface
['{2A307AD1-7465-4EA7-9245-1A87CE42F931}']
procedure DoSomething;
end;
TMyClass = class(TInterfacedObject, IMyInterface)
private
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.
In the example when the last interface reference is cleared, object destructor is being called.
It is possible to replace inherited _AddRef
and _Release
methods implementation with your own code. This is required for example when you need to use interfaces but don't need to reference count them. See the next example when _AddRef
and _Release
methods implementation is replaced and no reference counting is being done:
type
IMyInterface = interface
['{2A307AD1-7465-4EA7-9245-1A87CE42F931}']
procedure DoSomething;
end;
TMyClass = class(TInterfacedObject, IMyInterface)
private
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
procedure DoSomething;
public
constructor Create;
destructor Destroy; override;
end;
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.
When object is inherited from TComponent
it can implement interfaces, but it is not reference counted. It is like this because for components it is preferrable to use object lifetime manging based on owner.