Delphiのマネージド レコードとは?

Delphiのレコード型では、任意のデータ型フィールドを持つことができます。レコードが数値型や列挙値などの(管理されない)単純なデータ型フィールドを持つ場合、コンパイラが行うことはあまりありません。レコードの作成と破棄は、メモリ割り当てと割り当てたメモリ領域の解放のみです(Delphiでは、デフォルトでレコードをゼロ初期化しない点に注意してください)。

レコードが、コンパイラによって管理されるデータ型のフィールド(string型やinterfaceなど)を持つ場合、コンパイラは初期化やファイナライズを管理するためのコードを挿入しなければなりません。例えば、string型は参照カウントされるため、レコードがスコープから外れたときに、レコード内のstring型フィールドの参照カウントを減らす必要があり、その結果、このフィールドのメモリの割り当てが解除される可能性もあります。そのため、コードの中でそのようなマネージド レコードを使用している場合には、コンパイラは自動的にそのコードブロックをtry-finallyで囲い、例外が発生した場合でもデータが確実にクリアされるようにします。このような動作は、長い期間サポートされてきました。つまり、マネージド レコードはDelphi言語の一部になっていました。

InitializeおよびFinalize演算子を持つレコード

10.4では、新たにDelphiのレコード型で、カスタム初期化ならびにファイナライズをサポートするようになりました。これにより、マネージド レコードに対して、コンパイラのデフォルト操作を上書きすることができます。フィールドのデータ型にかかわらず、カスタム初期化およびファイナライズコードを伴ってレコードを宣言することができ、初期化とファイナライズのためのカスタムコードを記述できます。これは、特定の新しい演算子をレコード型に追加することによって可能になります(不要な場合には、いずれかの演算子の省略も可能です)。

以下は、シンプルなコード例です。

type
  TMyRecord = record
    Value: Integer;
    class operator Initialize (out Dest: TMyRecord);
    class operator Finalize(var Dest: TMyRecord);
  end;

もちろん、対応する2つのクラス メソッド コードを記述しなければなりません。例えば、実行ログの出力やレコード値の初期化などを記述します。この例では、メモリロケーションへの参照もログに出力し、どのレコードに対して操作を行っているのかを確認できるようにしています。

class operator TMyRecord.Initialize (out Dest: TMyRecord);
begin
  Dest.Value := 10;
  Log('created' + IntToHex (Integer(Pointer(@Dest)))));
end;

class operator TMyRecord.Finalize(var Dest: TMyRecord);
begin
  Log('destroyed' + IntToHex (Integer(Pointer(@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 を使います。

  class operator Assign (var Dest: TMyRecord; const [ref] Src: TMyRecord);

この演算子の定義は、非常に正確なルールに則って行う必要があります。最初のパラメータは参照パラメータ、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

以下は、Assign演算子を用いた例です。

var
  my1, my2: TMyRecord;
begin
  my1.Value := 22;
  my2 := my1;

この結果、以下のようなログが出力されます(レコードにシーケンス番号も追加してみました)。

created 5 0019F2A0
created 6 0019F298
5 copied to 6
destroyed 6 0019F298
destroyed 50019F2A0

破棄の順番が、生成とは逆の順序になっていることに注意してください。

マネージド レコードをパラメータとして渡す

マネージド レコードは、パラメータとして渡されたり、関数の戻り値として返される場合にも、通常のレコードとは異なる動作が可能です。以下は、さまざまなシナリオの例です。

procedure ParByValue (rec: TMyRecord);
procedure ParByConstValue (const rec: TMyRecord);
procedure ParByRef (var rec: TMyRecord);
procedure ParByConstRef (const [ref] rec: TMyRecord);
function ParReturned: TMyRecord;

ここでは、ひとつひとつのログを確認することはせず、以下に要約することにします。

  • ParByValue は、新しいレコードを作成し、割り当て演算子がある場合には、これを呼び出しデータをコピーします。そして、プロシージャを終了するときにこの一時コピーを破棄します。
  • PParByConstValue は、コピーを行わず、何も呼び出されません。
  • ParByRef も、コピーを行わず、何も呼び出されません。
  • ParByConstRef も、コピーを行わず、何も呼び出されません。
  • ParReturned は、(Initializeを用いて)新しいレコードを作成し、Assign演算子を呼び出すことでそれを返します。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

まとめ

ここでは、エンバカデロが次の10.4リリースで、Delphi言語に追加するすばらしい新機能について簡単に紹介しました。マネージド レコードは、例えばジェネリック レコードをはじめ、ここでは紹介しきれないような、さまざまなシナリオで活用できます。ここで紹介した新機能は、言語に関するものでしたが、このほかにも、すべてのプラットフォームで共通したメモリ管理などもあります。ぜひ、ご期待ください。


アップデートサブスクリプションに加入している方は、ベータビルドへのアクセスが可能です。現在でも10.4のベータプログラムに参加可能ですので、お問い合わせください。

この記事は、RAD Studioの将来のリリースに関するプレビューです。製品品質やスケジュール等の理由により、紹介した機能の提供が変更になる可能性があります。製品が正式にリリースされるまで、最終的な機能、スペック等について保証されません。