Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

Managing object lifetime in Delphi

5.00/5 (3 votes)
4 Nov 2015CPOL6 min read 17.4K  
In the article I try to make a brief review over methods to manage object lifetime in Delphi applications.

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:

Pascal
type
  TMyObject = class
  public
    // Конструктор.
    constructor Create;
    { TObject }
    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.

Pascal
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:

Pascal
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;

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:

Pascal
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.

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:

Pascal
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.

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)