Table of Contents
Delphiのマネージド レコードとは?
Delphiのレコード型では、任意のデータ型フィールドを持つことができます。レコードが数値型や列挙値などの(管理されない)単純なデータ型フィールドを持つ場合、コンパイラが行うことはあまりありません。レコードの作成と破棄は、メモリ割り当てと割り当てたメモリ領域の解放のみです(Delphiでは、デフォルトでレコードをゼロ初期化しない点に注意してください)。
レコードが、コンパイラによって管理されるデータ型のフィールド(string型やinterfaceなど)を持つ場合、コンパイラは初期化やファイナライズを管理するためのコードを挿入しなければなりません。例えば、string型は参照カウントされるため、レコードがスコープから外れたときに、レコード内のstring型フィールドの参照カウントを減らす必要があり、その結果、このフィールドのメモリの割り当てが解除される可能性もあります。そのため、コードの中でそのようなマネージド レコードを使用している場合には、コンパイラは自動的にそのコードブロックをtry-finallyで囲い、例外が発生した場合でもデータが確実にクリアされるようにします。このような動作は、長い期間サポートされてきました。つまり、マネージド レコードはDelphi言語の一部になっていました。
InitializeおよびFinalize演算子を持つレコード
10.4では、新たにDelphiのレコード型で、カスタム初期化ならびにファイナライズをサポートするようになりました。これにより、マネージド レコードに対して、コンパイラのデフォルト操作を上書きすることができます。フィールドのデータ型にかかわらず、カスタム初期化およびファイナライズコードを伴ってレコードを宣言することができ、初期化とファイナライズのためのカスタムコードを記述できます。これは、特定の新しい演算子をレコード型に追加することによって可能になります(不要な場合には、いずれかの演算子の省略も可能です)。
以下は、シンプルなコード例です。
1 2 3 4 5 6 |
type TMyRecord = record Value: Integer; class operator Initialize (out Dest: TMyRecord); class operator Finalize(var Dest: TMyRecord); end; |
もちろん、対応する2つのクラス メソッド コードを記述しなければなりません。例えば、実行ログの出力やレコード値の初期化などを記述します。この例では、メモリロケーションへの参照もログに出力し、どのレコードに対して操作を行っているのかを確認できるようにしています。
1 2 3 4 5 6 7 8 9 10 |
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ブロック内で実行されます。
1 2 3 4 5 6 |
procedure LocalVarTest; var my1: TMyRecord; begin Log (my1.Value.ToString); end; |
このコードを実行すると、次のようなログが出力されます。
1 2 3 |
created 0019F2A8 10 destroyed 0019F2A8 |
別のシナリオは、インライン変数として使用するケースです。
1 2 3 |
begin var t: TMyRecord; Log(t.Value.ToString); |
この場合でも同じようなログが出力されます。
演算子の割り当て
代入演算子 := には、レコードのすべてのフィールドデータをそのままコピーする動作が割り当てられています。この動作は、デフォルトとして妥当ですが、カスタムデータフィールドとカスタム初期化を持つ場合には、この動作を変更する必要が出てくるかもしれません。これが、カスタム マネージド レコード向けに、代入演算子を定義できる理由になります。新しい演算子は := 構文で使用できますが、定義には、Assign を使います。
1 |
class operator Assign (var Dest: TMyRecord; const [ref] Src: TMyRecord); |
この演算子の定義は、非常に正確なルールに則って行う必要があります。最初のパラメータは参照パラメータ、2番目のパラメータは参照渡しのconst型になります。この規則に従わない場合、コンパイラは以下のようなエラーメッセージを出力します。
1 2 |
[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演算子を用いた例です。
1 2 3 4 5 |
var my1, my2: TMyRecord; begin my1.Value := 22; my2 := my1; |
この結果、以下のようなログが出力されます(レコードにシーケンス番号も追加してみました)。
1 2 3 4 5 |
created 5 0019F2A0 created 6 0019F298 5 copied to 6 destroyed 6 0019F298 destroyed 50019F2A0 |
破棄の順番が、生成とは逆の順序になっていることに注意してください。
マネージド レコードをパラメータとして渡す
マネージド レコードは、パラメータとして渡されたり、関数の戻り値として返される場合にも、通常のレコードとは異なる動作が可能です。以下は、さまざまなシナリオの例です。
1 2 3 4 5 |
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ブロックが存在しない場合でも、例外が発生したときには、一般的にレコードはクリアされます。これはオブジェクトとは異なる動作です。これは根本的な違いであり、マネージド レコードの実際の有用性のポイントです。
1 2 3 4 5 6 |
procedure ExceptionTest; begin var a: TMRE; var b: TMRE; raise Exception.Create('Error Message'); end; |
このプロシージャ内では、コンストラクタ呼び出しが2回、デストラクタ呼び出しが2回発生します。繰り返しますが、これがマネージド レコードの根本的な違いであり、ポイントとなる機能です。マネージド レコードをベースとした、簡単なスマートポインタについては、次のセクションを参照ください。
マネージド レコードの配列
マネージド レコードの静的配列を定義すると、宣言時に初期化演算子を用いた初期化を行うことができます。
1 2 3 4 |
var a1: array [1..5] of TMyRecord; // call here begin Log ('ArrOfRec'); |
この配列は、スコープから外れるとすべて破棄されます。マネージド レコードの動的配列を宣言すると、初期化コードは、(SetLengthを使って)配列サイズを設定した際に呼び出されます。
1 2 3 4 5 |
var a2: array of TMyRecord; begin Log ('ArrOfDyn'); SetLength(a2, 5); // call here |
まとめ
ここでは、エンバカデロが次の10.4リリースで、Delphi言語に追加するすばらしい新機能について簡単に紹介しました。マネージド レコードは、例えばジェネリック レコードをはじめ、ここでは紹介しきれないような、さまざまなシナリオで活用できます。ここで紹介した新機能は、言語に関するものでしたが、このほかにも、すべてのプラットフォームで共通したメモリ管理などもあります。ぜひ、ご期待ください。
アップデートサブスクリプションに加入している方は、ベータビルドへのアクセスが可能です。現在でも10.4のベータプログラムに参加可能ですので、お問い合わせください。
この記事は、RAD Studioの将来のリリースに関するプレビューです。製品品質やスケジュール等の理由により、紹介した機能の提供が変更になる可能性があります。製品が正式にリリースされるまで、最終的な機能、スペック等について保証されません。
Design. Code. Compile. Deploy.
Start Free Trial Upgrade Today
Free Delphi Community Edition Free C++Builder Community Edition