型可変オープン配列パラメータの使い方

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;

こんな感じ。

さて、型可変オープン配列パラメータに関してちょこっとメモ書きをする程度のつもりだったのだが、
随分と長話になってしまったようだ。ではごきげんよう