はじめに
本記事は、以下の記事で紹介したスマートポインタ風テクニックを応用して汎用的に使えるオブジェクトを設計しました
Delphiで複数のインスタンスを生成して処理を行う際、例外処理(try...finally)を書いたり、最後に1つずつ Free を呼ぶのは地味に面倒で、解放漏れ(メモリリーク)の原因にもなります。
「record と IInterface の参照カウントを組み合わせたスマートポインタ風のテクニック」を応用し、「変数宣言1つだけ+コンストラクタ呼び出し不要+代入文すら不要+スコープ終了時に全自動Free」 できる汎用コンテナ(TAutoFreeObjects<T>)を作りました。
従来(Before)とこれから(After)
Before:従来のコード
生成した数だけ try...finally や Free を書く必要があり、コードが長くなりがちでした。
procedure ProcessDemo;
var
A, B: TMyObject;
begin
A := TMyObject.Create('A');
try
B := TMyObject.Create('B');
try
A.DoSomething;
B.DoSomething;
finally
B.Free;
end;
finally
A.Free;
end;
end;
After:TAutoFreeObjects を使ったコード
AutoFree 変数を1個宣言して .Add() に渡すだけ。コンストラクタ呼び出しも、代入文(AutoFree := ...)の記述も、Free も一切不要です!
procedure ProcessDemo;
var
AutoFree: TAutoFreeObjects<TMyObject>; // ★変数宣言はこれ1つだけ!
begin
// 代入文の左辺(AutoFree := ...)すら不要。いきなり Add してOK!
AutoFree.Add(TMyObject.Create('A')).DoSomething;
AutoFree.Add(TMyObject.Create('B')).DoSomething;
// 関数(スコープ)を抜けた瞬間に、A と B が自動的に Free される!
end;
コンテナユニットの実装コード(AutoFreeObjects.pas)
ユニットとして独立して使えるようにまとめました。
unit AutoFreeObjects;
interface
uses
System.SysUtils, System.Generics.Collections;
type
// スコープ終了時に追加されたオブジェクトを全自動で Free するスマートコンテナ
TAutoFreeObjects<T: class> = record
private
type
// 内部限定の寿命管理用クラス
TObjectOwner = class(TInterfacedObject)
private
FItems: TObjectList<T>;
public
constructor Create;
destructor Destroy; override;
function Add(AItem: T): T;
end;
private
FOwner: TObjectOwner;
FRef: IInterface;
function GetOwner: TObjectOwner;
public
// オブジェクトを管理下に追加し、追加されたインスタンスをそのまま返す
function Add(AItem: T): T;
end;
implementation
{ TAutoFreeObjects<T>.TObjectOwner }
constructor TAutoFreeObjects<T>.TObjectOwner.Create;
begin
inherited Create;
// OwnsObjects = True(デフォルト)により、リスト破棄時に中身も自動Freeされる
FItems := TObjectList<T>.Create(True);
end;
destructor TAutoFreeObjects<T>.TObjectOwner.Destroy;
begin
FItems.Free;
inherited;
end;
function TAutoFreeObjects<T>.TObjectOwner.Add(AItem: T): T;
begin
FItems.Add(AItem);
Result := AItem;
end;
{ TAutoFreeObjects<T> }
function TAutoFreeObjects<T>.GetOwner: TObjectOwner;
begin
// 初回アクセス時に自動初期化(遅延初期化)
if FRef = nil then
begin
FOwner := TObjectOwner.Create;
FRef := FOwner; // インターフェース参照を入れて参照カウント開始
end;
Result := FOwner;
end;
function TAutoFreeObjects<T>.Add(AItem: T): T;
begin
Result := GetOwner.Add(AItem);
end;
end.
仕組み・こだわったポイント
1. 代入文(AutoFree := ...)すら不要な理由(遅延初期化)
Delphiの record は引数なしのコンストラクタが作れないため、普通に設計すると AutoFree := TAutoFreeObjects<T>.Create; のような代入文が必要になります。
しかし本実装では、GetOwner 内で 「初めて Add が呼ばれたときに nil チェックして内部オブジェクトを自動初期化する(遅延初期化)」 仕組みを採用しました。
そのため、代入文の左辺として書き始める必要すらなく、変数宣言だけ済ませたら直接 .Add() を呼び出すだけで機能します。
2. 解放のドミノ倒し(なぜスコープ終了で一括Freeされるのか?)
Delphiの record はスタック型のため、変数が宣言されたスコープ(関数など)を抜けるタイミングでメモリから消滅します。
このとき、内部で保持している 唯一の IInterface 参照(FRef) が解放され、参照カウントが 0 になって内部の TObjectOwner が自動的に Destroy されます。
TObjectOwner は内部に TObjectList<T>(True) を保持しているため、親コンテナが破棄される連鎖反応(ドミノ倒し)によって、リストの中に溜め込まれていたオブジェクト群がすべて自動で Free されます。
3. Add がオブジェクト自身を返す設計
Add の戻り値を T(渡されたオブジェクト型)にしているため、AutoFree.Add(TSample.Create('A')).DoSomething;のように変数に代入しなくてもメソッドを呼び出すことができます。
ObjA := AutoFree.Add(TSample.Create('B'));
にようにオブジェクト変数に直接代入することも出来ます
var
AutoFree: TAutoFreeObjects<TSample>;
ObjA: TSample;
begin
// 1. 登録したその場でメソッドを呼び出す(流れるような記述)
AutoFree.Add(TSample.Create('A')).DoSomething;
// 2. 個別変数で受けて後から使うことも可能
ObjA := AutoFree.Add(TSample.Create('B'));
ObjA.DoSomething;
end;
おわりに
TAutoFreeObjects<T> を使うことで、try...finally や .Free の連発から解放され、安全かつ直感的にオブジェクトを扱えるようになりました。
「ただ一時的なオブジェクトをいくつか生成して、スコープの終わりで安全に消えてほしい」という場面で強力な武器になりますので、ぜひ活用してみてください!
おまけ:本当に解放されているか試してみよう!
「本当に Free 書かなくて大丈夫なの…?」と不安な方向けに、デストラクタでログを出力する検証プログラムを用意しました。
検証コード
program AutoFreeTest;
{$APPTYPE CONSOLE}
uses
System.SysUtils,
AutoFreeObjects in 'AutoFreeObjects.pas';
type
TSampleObject = class
private
FName: string;
public
constructor Create(const AName: string);
destructor Destroy; override;
procedure Work;
end;
constructor TSampleObject.Create(const AName: string);
begin
FName := AName;
Writeln('[生成] ', FName);
end;
destructor TSampleObject.Destroy;
begin
Writeln('[自動Free] ', FName, ' が破棄されました!');
inherited;
end;
procedure TSampleObject.Work;
begin
Writeln(' └> ', FName, ' がお仕事をしています');
end;
procedure RunScope;
var
AutoFree: TAutoFreeObjects<TSampleObject>;
SampleObject: TSampleObject;
begin
Writeln('--- スコープ開始 ---');
// 生成してその場でお仕事
AutoFree.Add(TSampleObject.Create('オブジェクトA')).Work;
AutoFree.Add(TSampleObject.Create('オブジェクトB')).Work;
SampleObject := AutoFree.Add(TSampleObject.Create('オブジェクトC'));
SampleObject.Work;
Writeln('--- スコープ終了直前 ---');
end; // ← ここでスコープ終了!
begin
RunScope;
Writeln('--- スコープを抜けました ---');
Readln;
end.
実行結果
--- スコープ開始 ---
[生成] オブジェクトA
└> オブジェクトA がお仕事をしています
[生成] オブジェクトB
└> オブジェクトB がお仕事をしています
[生成] オブジェクトC
└> オブジェクトC がお仕事をしています
--- スコープ終了直前 ---
[自動Free] オブジェクトA が破棄されました!
[自動Free] オブジェクトB が破棄されました!
[自動Free] オブジェクトC が破棄されました!
--- スコープを抜けました ---
ご覧の通り、--- スコープ終了直前 --- と --- スコープを抜けました --- の ちょうど境目(関数を抜けた瞬間) で、追加したすべてのオブジェクトが綺麗に Free されていることが分かります!
安心して .Free の記述をポイしちゃいましょう!