Autorelease Pool
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;
Why won't one just use IShared from Spring4D? Admittedly, AutoRelease is slightly faster and comes with less overhead but that will rarely be the deciding factor.
ReplyDeleteWhere is fun in that ;)
DeleteSmart pointers can be used, too. But their usage is a bit different and because of that implementations are tad more complex.
My goal is presenting simplest solution for particular use case. In other words, how to achieve something. If someone is already using some library that has similar solution, even if more complicated, then it is easier to use that library than reinventing the wheel.
On the other hand introducing library as dependency just to use some simple thing may be an overkill.
Guard from JCL
ReplyDeleteAutoFree from mORMot