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

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

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

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

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

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

[crayon-67686371b72fb720270810/]

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

[crayon-67686371b7305536574749/]

このコンストラクタ メカニズムと従来のレコードとの大きな違いは、自動呼び出しに関する差です。以下のコードのような記述を行うと、イニシャライザとファイナライザの両方を呼び出すことができ、最終的に、マネージド レコードのインスタンス用にコンパイラが生成したtry-finallyブロック内で実行されます。

[crayon-67686371b7308939372679/]

このコードを実行すると、次のようなログが出力されます。

[crayon-67686371b730a089516160/]

別のシナリオは、インライン変数として使用するケースです。

[crayon-67686371b730b601917356/]

この場合でも同じようなログが出力されます。

演算子の割り当て

代入演算子 := には、レコードのすべてのフィールドデータをそのままコピーする動作が割り当てられています。この動作は、デフォルトとして妥当ですが、カスタムデータフィールドとカスタム初期化を持つ場合には、この動作を変更する必要が出てくるかもしれません。これが、カスタム マネージド レコード向けに、代入演算子を定義できる理由になります。新しい演算子は := 構文で使用できますが、定義には、Assign を使います。

[crayon-67686371b730d189690565/]

この演算子の定義は、非常に正確なルールに則って行う必要があります。最初のパラメータは参照パラメータ、2番目のパラメータは参照渡しのconst型になります。この規則に従わない場合、コンパイラは以下のようなエラーメッセージを出力します。

[crayon-67686371b730f030218477/]

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

[crayon-67686371b7311740776092/]

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

[crayon-67686371b7312022952660/]

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

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

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

[crayon-67686371b7314632011400/]

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

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

例外とマネージド レコード

明示的なtry-finallyブロックが存在しない場合でも、例外が発生したときには、一般的にレコードはクリアされます。これはオブジェクトとは異なる動作です。これは根本的な違いであり、マネージド レコードの実際の有用性のポイントです。

[crayon-67686371b7316974536616/]

このプロシージャ内では、コンストラクタ呼び出しが2回、デストラクタ呼び出しが2回発生します。繰り返しますが、これがマネージド レコードの根本的な違いであり、ポイントとなる機能です。マネージド レコードをベースとした、簡単なスマートポインタについては、次のセクションを参照ください。

マネージド レコードの配列

マネージド レコードの静的配列を定義すると、宣言時に初期化演算子を用いた初期化を行うことができます。

[crayon-67686371b7317532936289/]

この配列は、スコープから外れるとすべて破棄されます。マネージド レコードの動的配列を宣言すると、初期化コードは、(SetLengthを使って)配列サイズを設定した際に呼び出されます。

[crayon-67686371b7319274663537/]

まとめ

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


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

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