型可変オープン配列パラメータの使い方
Format関数などを利用していると、型可変オープン配列パラメータを利用することが多い。
例えばFormat関数の場合は、
Format('文字列%s 整数%d 小数%f', ['abc', 15, 0.5])
とすると、'文字列abc 整数15 小数0.5'が帰ってくる。実に便利だ。
型可変オープン配列パラメータを利用すると、どんな型でも(正確には制限はあるが)とりあえずパラメータにぶち込むことが出来るのだが、一つだけ制約条件がある。
「array of const」は内部的には「array of TVarRec」の糖衣構文であるため、
array of constの動的配列をプログラム上で作成するのが面倒であるということ。
それこそ、
var Array: array of const; i: Integer; begin SetLength(Array, 2); Array.Add('abc'); Array.Add(15); Array.Add(0.5); ShowMessage(Format('文字列%s 整数%d 小数%f', Array); end;
みたいなことが出来れば楽なのだが、そうは問屋が卸さない。
手動でTVarRecレコード(の配列)に代入をしていかなければならない。
さて、肝心のTVarRecの定義を覗いてみると(System.pas)、
TVarRec = record { do not pack this record; it is compiler-generated } case Byte of vtInteger: (VInteger: Integer; VType: Byte); vtBoolean: (VBoolean: Boolean); vtChar: (VChar: Char); vtExtended: (VExtended: PExtended); vtString: (VString: PShortString); vtPointer: (VPointer: Pointer); vtPChar: (VPChar: PChar); vtObject: (VObject: TObject); vtClass: (VClass: TClass); vtWideChar: (VWideChar: WideChar); vtPWideChar: (VPWideChar: PWideChar); vtAnsiString: (VAnsiString: Pointer); vtCurrency: (VCurrency: PCurrency); vtVariant: (VVariant: PVariant); vtInterface: (VInterface: Pointer); vtWideString: (VWideString: Pointer); vtInt64: (VInt64: PInt64); end;
とある。'case Byte of'とか普通のDelphi参考書には載っていないのだが、
いわゆる「ユニオン(レコード型可変)」と呼ばれるものだ。
構文がやたら複雑なのだが、やっていることは単純だ。
シンプルに書き換えてみよう
TVarRec = record VInteger: Integer; VType: Byte; end;
これだけ。初めの4バイトにInteger型のフィールドが用意され、続く1バイト('do not pack this record'とあるのでメモリ管理上は4バイトを占める)に数値(0〜16)が入る。
ただし、これだとVIntegerにはInteger型の数値しか格納できない。
だからといって、例えば
TVarRec = record VType: Byte; VInteger: Integer; VBoolean: Boolean; VChar: Char; ... VInt64: Int64; end;
と書いて、VTypeの値によって、使うフィールドを決める(例えばVType = 0ならVIntegerが有効で、VType = 1ならVBooleanが・・・)というのも頭が悪い。何よりメモリを無駄に消費する。
ここでポイントとなるのは、VInteger、VBoolean、VChar、・・・は決して同時には使われないということだ。
ならば、これらのフィールドのメモリを共有してしまえば良い。
それが、System.pasで定義されているTVarRecの実体だ。
'case Byte of'とか、'vtXXX'ってところは一切合切無視して(コンパイラ上でも全く意味が無い部分)、
VTypeがレコードの4バイト目を占め、
残りのVInteger、VBoolean、VChar、・・・ってのがレコードの0〜3バイト目(小さな型の場合は0バイト目だけ、あるいは0〜2バイト目を占め、残りが余るケースもある)を(共有して)占めているということだ。
これで、合わせて5バイト(8バイト)しか占めなくて済むのでコンパクトだ。
ところが、この方式がくせ者なのは、メモリの4バイト目にVTypeが鎮座しているということだ。
そうすると、共有することが出来るメモリは0〜3バイト目までの4バイトしかないということになる。
さて、Integer型は4バイトを占めるし、Char型などは1バイトしか占めないので良いのであるが、
例えばExtended型は10バイトを占めるし、ShortString型に至っては最大256バイトを占める。
4バイト以上のデータをそのまま格納することは出来ないので、ではどうするか。
そこでポインタの出番である。ポインタのサイズは4バイトだ。
格納するデータ本体はメモリのどこかに格納して、どこに格納しているかという情報をTVarRecに格納する。
4バイト以上を占めるものに関しては、例外なくこの方法が使われている。
(ちなみに、PCharとPWideChar型は長い文字列を格納するのに使う。その割にはPWideStringが用意されていたり、PAnsiStringが用意されていなかったり謎な仕様だがw)
というわけで、ポインタの使い方がわからないと、型可変オープン配列パラメータを使う事ができない。
ポインタはプログラミングの最初の壁ということで有名なのだが、やっていることは案外簡単だ。
var P: ^Extended; // ^Extendedの代わりにPExtendedでも可 begin GetMem(P, SizeOf(P)); P^ := 0.5; // P^を使うコード FreeMem(P); end;
これだけ。
ちなみに、ポインタで「@」演算子というものがあるが、Delphiで使う事はまずない。教科書などでは
var P: ^Extended; E: Extended; begin P := @E; P^ := 0.5 // P^を使うコード end;
といったソースが載っていたりもするが、この場合、Pが指定する先、即ちEのメモリはendのところで
問答無用で解放されてしまうので、外に持ち出すことが出来ない。
外に持ち出さないのならポインタを使う意味がないので、全くナンセンスなコードである。
@演算子は金輪際忘れて貰っても構わない。持ち越さないのなら初めから
var E: Extended; begin // Eを使うコード end;
って書けってんだ。(最初のコードではFreeMemで解放しない限りはどこまでも持ち出せる。)
細かい文法はともかく、押さえておかないとならないのは、
- ポインタを使用する場合は、GetMemでメモリを確保しないとならないこと。
- GetMemで確保されたメモリは放っておいても解放されないので、FreeMemを使って自分で解放しないといけないこと。
この辺が面倒なのだ。
さて前置きが異様に長くなってしまったが、
var S: String; begin S := Format('文字列%s 整数%d 小数%f', ['abc', 15, 0.5]); ShowMessage(S); end;
と同等のソースを糖衣構文を使わない方法で記述してみよう。
var Param: array of TVarRec; S: String; begin SetLength(Param, 3); with Param[0] do begin VType := vtAnsiString; VAnsiString^ := 'abc'; end; with Param[1] do begin VType := vtInteger; VInteger := 15; end; with Param[2] do begin VType := vtExtended; GetMem(VExtended, SizeOf(Extended)); VExtended^ := 0.5; end; S := Format('文字列%s 整数%d 小数%f', Param); ShowMessage(S); FreeMem(Param[2].VExtended); end;
な、長い・・・
ちなみに、最後のFreeMemを忘れてもプログラムそのものは動作するが、
解放を忘れる度にメモリがどんどん消費されてゆくことに注意。
ちなみに、今回のケースでは
var Param: array of TVarRec; S: String; E: Extended; begin SetLength(Param, 3); with Param[0] do begin VType := vtAnsiString; VAnsiString^ := 'abc'; end; with Param[1] do begin VType := vtInteger; VInteger := 15; end; E := 0.5; with Param[2] do begin VType := vtExtended; VExtended := @E; end; S := Format('文字列%s 整数%d 小数%f', Param); ShowMessage(S); end;
と書くことも可能である。この場合は、最後のendでEが自動的に解放されるので、FreeMemの様なことは不要だ(やってはいけない)
さて、Delphiの良いところでもあり、悪いところでもあるのが、中途半端に親切丁寧なメモリ管理機能だ。
先のソースでは自動管理のポインタが2カ所隠れている。
SetLengthのくだり(動的配列)と、文字列の部分だ。動的配列も、文字列も内部的にはポインタを利用している。
これらのポインタの解放タイミングは、「誰も使わなくなったとき」である。
SetLength(Param, 3)でメモリのどこかに確保された24バイト(8×3バイト、正確には+α)のデータは、
確保された時点で、Paramが参照している。この時、Paramが参照されるデータの参照カウンタと呼ばれるデータが1増加する。
ソースのendに達すると、ポインタであるParamが解放される。
この時、Paramの参照先であるデータの参照カウンタが1減少して、0になる。
0になると、そのデータがどこからも参照されなくなったということを意味するので、
このタイミングでDelphiは内部でメモリを解放するのだ。
文字列型に関しても同様で、'abc'はendに達してParamが解放され、続いてParamの中身が解放される。
この時Param[0].VAnsiStringも解放されるので、VAnsiStringが参照している'abc'の参照カウンタも0になって自動的に解放されるのだ。
じゃあVExtended(PExtended型)も同じように参照カウンタ管理してくれよと言いたいのだが、
残念ながら、Delphiはこうした型を参照カウンタ管理してくれることはない。
だから自分でちゃんと解放しなければならない。
ちなみに、オブジェクトのインスタンスをちゃんとFreeで解放してやらないといけないのも同じ理由。
ただし、インターフェイスを利用する場合に限っては、Freeを呼び出さなくても、参照カウンタで自動開放される。
例えば、
var I: Interface; begin I := TInterfacedObject.Create; // Iを使うコード // I.FreeやI.Destroyは出来ないので、明示的に解放する方法がない。 end;
こんな感じ。
さて、型可変オープン配列パラメータに関してちょこっとメモ書きをする程度のつもりだったのだが、
随分と長話になってしまったようだ。ではごきげんよう。