Delphi Autorelease Pool

FireWind

Свой
Регистрация
2 Дек 2005
Сообщения
1,957
Реакции
1,199
Credits
4,009
Autorelease Pool
February 23, 2021 by Dalija Prasnikar

Manual memory management requires some thought and ceremony. It is not so much of a problem with long-lasting instances, but managing temporary local objects, especially when you need to create more than one, is a tedious job. It also hurts code readability.

While Delphi allows automatic memory management for classes that implement interfaces, using ARC is not always feasible. If you have to deal with preexisting classes that don't support ARC, you will have to deal with manual memory management. Reference counting also adds some overhead, and using reference counting in your own classes is not always a viable approach.

When performance is not paramount, simpler and cleaner code without try...finally blocks would be nice to have. Life always finds a way and various ARC-based smart pointer and similar lifetime management wrapper implementations have started to appear in the wild.

One common place where constructing many object instances can be found is unit testing. While lifetime wrappers work well in applications, they often wrap object instances too well, and that can create issues in unit testing.

There is another approach to lifetime management that does not require wrapping the managed object - using some kind of auto-release pool that will take ownership (and memory management) over object instances, while keeping the original reference intact. In the case of single objects, there is no need for a pool and maintaining the list.

Implementing such auto-release managers is rather simple. Managers on their own are reference-counted: Their memory is automatically managed, and will be released when they go out of scope. To ensure proper construction and reference counting initialization for the TAutoRelease and TAutoReleasePool classes, their declaration will be hidden in the implementation section of a unit, and they can be constructed only through the AutoRelease record's static functions.

The TAutoRelease class can manage the lifetime of a single object instance. Since the compiler maintains a hidden interface reference in the method scope, there is no need for declaring an additional variable.

procedure TestXXX.TestYYY;
var
Foo: TFoo;
begin
Foo := TFoo.Create;
AutoRelease.New(Foo);
...
// use Foo

end; // The TAutoRelease object will be destroyed at this point, and will release Foo

The TAutoReleasePool class can manage the lifetime of multiple object instances. To use a pool, we need to keep a reference to the pool. In a unit testing environment, the pool can be a field in the test class that will be created and destroyed for every test in the SetUp and TearDown methods:

TestXXX = class(TTestCase)
strict private
AutoPool: IAutoReleasePool;
...

procedure TestXXX.SetUp;
begin
inherited;
AutoPool := AutoRelease.NewPool;
end;

procedure TestXXX.TearDown;
begin
AutoPool := nil;
inherited;
end;

procedure TestXXX.TestYYY;
var
Foo: TFoo;
Bar: TBar;
begin
Foo := TFoo.Create;
AutoPool.Add(Foo);
Bar := TBar.Create;
AutoPool.Add(Bar);

...
// use Foo and Bar

end;

Using such auto-release managers is not limited to a unit testing environment, and they can be used in regular code instead of full lifetime management wrappers. They do require an extra line of code compared to smart pointer implementations, but they still simplify code and make try...finally blocks redundant.

And here is a full implementation of auto-release managers:

interface

type
IAutoReleasePool = interface
procedure Clear;
procedure Add(aInstance: TObject);
end;

AutoRelease = record
public
class function New(aInstance: TObject): IInterface; static;
class function NewPool: IAutoReleasePool; static;
end;

implementation

type
TAutoRelease = class(TInterfacedObject)
strict private
fInstance: TObject;
public
constructor Create(aInstance: TObject);
destructor Destroy; override;
end;

TAutoReleasePool = class(TInterfacedObject, IAutoReleasePool)
strict private
fInstances: TList<TObject>;
public
constructor Create;
destructor Destroy; override;
procedure Clear;
procedure Add(aInstance: TObject);
end;

constructor TAutoRelease.Create(aInstance: TObject);
begin
fInstance := aInstance;
end;

destructor TAutoRelease.Destroy;
begin
fInstance.Free;
inherited;
end;

constructor TAutoReleasePool.Create;
begin
fInstances := TObjectList<TObject>.Create;
end;

destructor TAutoReleasePool.Destroy;
begin
fInstances.Free;
inherited;
end;

procedure TAutoReleasePool.Clear;
begin
fInstances.Clear;
end;

procedure TAutoReleasePool.Add(aInstance: TObject);
begin
fInstances.Add(aInstance);
end;

class function AutoRelease.New(aInstance: TObject): IInterface;
begin
Result := TAutoRelease.Create(aInstance);
end;

class function AutoRelease.NewPool: IAutoReleasePool;
begin
Result := TAutoReleasePool.Create;
end;