カスタム管理レコード
目次
概要
Delphi におけるレコードは、どんなデータ型のフィールドも格納できます。レコードが、数値やその他の列挙値などのプレーンな(非管理)フィールドで構成されている場合には、コンパイラが行うことはあまりありません。レコードの作成と破棄は、メモリの割り当てまたはメモリ位置の削除で構成されています。
レコードにコンパイラが管理するタイプのフィールド(文字列やインターフェイスなど)がある場合、コンパイラは初期化またはファイナライズを管理するために追加のコードを挿入する必要があります。たとえば、文字列は参照がカウントされるため、レコードがスコープの外に出たとき、レコード内の文字列は自分の参照カウントを 1 つ減少させる必要があります。これにより、その文字列のために確保されたメモリの解放が引き起こされる可能性があります。このため、1 つのセクションのコードで、このような管理レコードを使用している場合、コンパイラは自動的に、try-finally ブロックをそのコードを囲むように追加し、例外が発生した場合でもデータが確実にクリアされるようにします。これは以前からのパターンです。いいかえると、管理レコードは、Delphi 言語の一部ともいえるのです。
初期化演算子とファイナライズ演算子と持つレコード
Delphi レコード型は、独自の初期化およびファイナライゼーションをサポートしており、これらは、管理レコードに対してコンパイラが行うデフォルト オペレーションより優先されます。独自の初期化コードおよびファイナライゼーション コードで、フィールドのデータ型に関係なく、レコードを宣言することができ、また、そのような独自の初期化およびファイナライゼーションのコードを記述することができます。 これは、特定の新しい演算子をレコード型に追加することで実現されます(必要に応じて、他の演算子をなくし、それ1つにすることも可能です)。以下は、簡単なコード スニペットです:
type
TMyRecord = record
Value: Integer;
class operator Initialize (out Dest: TMyRecord);
class operator Finalize (var Dest: TMyRecord);
end;
両クラス メソッドのコードを記述する必要があることを覚えておいてください。たとえば、実行のログをとる、またはレコード値を初期化するなどの場合、各オペレーションをどのレコードが実行しているのかを見るために、メモリの位置への参照のログもとります:
class operator TMyRecord.Initialize (out Dest: TMyRecord);
begin
Dest.Value := 10;
Log('created' + IntToHex (IntPtr(@Dest)));
end;
class operator TMyRecord.Finalize (var Dest: TMyRecord);
begin
Log('destroyed' + IntToHex (IntPtr(@Dest)));
end;
このコンストラクタと、以前レコードで利用可能だったものとの大きな違いは、自動的に呼出しされるかどうかです。 以下のようなコードを記述した場合、イニシャライザとファイナライザの両方を呼び出すことができ、管理レコードのインスタンスのためにコンパイラが生成した try-finally ブロックで終了します。
procedure LocalVarTest;
var
my1: TMyRecord;
begin
Log (my1.Value.ToString);
end;
このコードで、次のログを取得できます:
created 0019F2A8
10
destroyed 0019F2A8
別のシナリオは、インライン変数の使用で、次のようになります:
begin
var t: TMyRecord;
Log(t.Value.ToString);
ログで同じ流れとなります。
代入演算子
:= 代入演算子は、レコード フィールドのすべてのデータをコピーします。これは妥当なデフォルトですが、カスタム データ フィールドとカスタム初期化によって、この動作を変更することができます。これが、カスタム管理レコードで、代入演算子も定義できる理由です。新しい演算子は := 構文で呼び出されますが、Assign として定義されます:
type
TMyRecord = record
Value: Integer;
class operator Assign (var Dest: TMyRecord;
const [ref] Src: TMyRecord);
この演算子の定義は、第1パラメータに参照パラメータ、第2パラメータに参照渡しの const を指定するなど、非常に精密なルールに従う必要があります。この順守に失敗すると、コンパイラは次のようなエラー メッセージを表示します:
[dcc32 Error] E2617 First parameter of Assign operator must be a var parameter of the container type
[dcc32 Hint] H2618 Second parameter of Assign operator must be a const[Ref] or var parameter of the container type
代入演算子を呼び出すサンプル例があります:
var
my1, my2: TMyRecord;
begin
my1.Value := 22;
my2 := my1;
これは、次のログを生成します(シーケンス番号がレコードに含まれます):
created 5 0019F2A0
created 6 0019F298
5 copied to 6
destroyed 6 0019F298
destroyed 5 0019F2A0
破壊の流れが、生成の流れと逆になっている点に注目してください。
Assign 演算子は、上記の例のように代入演算子と一緒に使用され、インライン変数を初期化する代入を使用する場合もです。 ここで、2 つの異なるケースを見てみましょう:
var
my1: TMyRecord;
begin
var t := my1;
Log(t.Value.ToString);
var s: TMyRecord;
Log(s.Value.ToString);
これは次のようにログを出力します:
created 6 0019F2A8
created 7 0019F2A0
6 copied to 7
10
created 8 0019F298
10
destroyed 8 0019F298
destroyed 7 0019F2A0
destroyed 6 0019F2A8
最初のケースは、作成と代入が、非ローカル変数を持つ通常のシナリオで行われています。2 番目のケースでは、単なる通常の初期化が行われています。
管理レコードをパラメータとして渡す
管理レコードは、パラメータとして渡されたり、関数から返されたりした場合にも、通常のレコードとは異なる動作をすることができます。以下は、さまざまなシナリオを示すいくつかのルーチンです:
procedure ParByValue (rec: TMyRecord);
procedure ParByConstValue (const rec: TMyRecord);
procedure ParByRef (var rec: TMyRecord);
procedure ParByConstRef (const [ref] rec: TMyRecord);
function ParReturned: TMyRecord;
各ログは、次のオペレーションを実行します:
- ParByValue、新しいレコードを作成し、代入演算子(利用可能な場合)を呼び出してデータをコピーし、プロシージャの終了時に一時コピーを破棄します。
- ParByConstValue、コピーも呼び出しもしません。
- ParByRef、コピーも呼び出しもしません。
- ParByConstRef、コピーも呼び出しもしません。
- ParReturned、(初期化を介して)新しいレコードを作成し、呼び出しが次のような場合、戻り時に代入演算子を呼び出し、一時レコードを削除します。
my1 := ParReturned;
例外と管理レコード
例外が発生すると、オブジェクトとは異なり、明示的な try、finally ブロックがなくても、一般にレコードはクリアされます。これは根本的な違いであり、管理レコードの現実的な利点の鍵となっています。
procedure ExceptionTest;
begin
var a: TMRE;
var b: TMRE;
raise Exception.Create('Error Message');
end;
このプロシージャ内には、2 つのコンストラクタ呼び出しと 2 つのデストラクタ呼び出しがあります。今一度、これは根本的な違いであり、管理レコードの主要な機能と言えます。以降のセクションで、管理レコードをベースとした、シンプルでスマートなポインタについて参照してください。
一報で、管理レコードの初期化子で例外が発生した場合、対応するデストラクタは呼び出されず、通常のオブジェクトとは異なります。
管理レコードの配列
管理レコードの静的配列を定義した場合、宣言のタイミングで、初期化オペレータを呼び出して初期化されます。
var
a1: array [1..5] of TMyRecord; // call here
begin
Log ('ArrOfRec');
それらはスコープの外に出ると、すべて破壊されます。管理レコードの動的配列を定義した場合、初期化コードは、配列のサイズ指定(SetLength で)のタイミングで呼び出されます。
var
a2: array of TMyRecord;
begin
Log ('ArrOfDyn');
SetLength(a2, 5); // call here
C++ における Delphi 管理レコード
管理レコードは、C++ ではサポートされていません。コンポーネントなど、もし両言語からアクセス可能なコードを記述する必要がある場合、C++ からも見えるコード(つまり、宣言された型、メソッド パラメータ、または戻り型を含む、Delphi ユニットの interface セクション)で、通常のレコードのみを使用します。
Initialize
および Finalize
の演算子は、生成された HPP ファイルで宣言されますが、それらを起動するコンストラクタやデストラクタは生成されません。同様に、代入演算子は HPP で宣言されますが、対応するコピー コンストラクタや代入演算子は宣言されません。