./index.html ../index.html

ObjectPascal Magic Programming
Dynamic array's RTTI

Delphi4以降、動的配列があります。

動的配列の、対TListメリットとして、型が保証される、ポインタ以外にも使える、破棄が自動、肥大化の元凶Classes.pasをusesしないでも使える、などがあります。 Javaの影響かどうかは知りませんが、中途半端に参照型*1なのが玉に傷ですが、それ以外は(C++の)vectorのようなもんです。 欲を言えば、CopyだけではなくInsertやDeleteも使えるようにして欲しかったですが…。 push_backAddが無く、SetLengthだけですべてを行うのは、やはりやや不便です。 それでも、Javaが総称をサポートしたり、C#も(略)な現在、今更TListに戻る気はしません。

逆に、TList(やJavaの非genericなコンテナ)のほうが、動的配列(やC++のvector)よりも優れている点が、ただひとつあります。

静的に型を定めて、継承関係も無い場合、引き渡しが不便なのです。

TListBoxやTMemoのItemsやLinesは、TStrings派生です。 多態してます。 これがもし、このような構造になってなかったら…。

TStringsを処理する汎用ルーチンなんかは、templateがあれば書けてしまいます。 ただし、templateの場合、ルーチン側も型が非互換なものが複数生成されてしまいます。 渡す先が、巨大なクラスライブラリの、ひとつのクラスのメソッド…だったらtemplateにできるなあ…じゃ、コンストラクタということで。コンストラクタだったらどうでしょう。 あるいは、フィールド(C++用語ですとメンバ変数)に持っておきたい、等のケースを想定してください。 不特定多数の型を扱いたいからといって、そのクラスまでもtemplateにしてしまうわけにはいきません。 そうなるとそのクラスのポインタを持つクラスまで引きずられて…。

一応書いておきますが、Delphiにtemplateは無いので念のため。 総称対動的型の一般論のつもりです。

ともかく、こういう場合は、複数の型をひとつの型として取り扱えるメカニズムが必要です。 C++ではアダプタだけをtemplateで作れば解決ですね。OCamlとかはどうなんでしょう。

…延々(自分でも)意味不明な事を書いて、何が言いたいかといいますと、様々な要素型の異なる動的配列郡を、TListのように一括して扱えたら便利な事がある、ってだけなのですが…。 C#なんかですと、動的配列がICollectionを実装してくれてたりしますけど、要するにそういうことです。 最初からそう書けば良かった。

強引に進めてしまいましょう…。 ともかく、Delphiの動的配列は、型安全を犠牲にする代わりに、要素型を問わずにすべての動的配列を取り扱えるようなルーチンを書くことができます。 RTTI万歳。

実際には、動的配列はPointerにキャストしてvarパラメータで渡します。 この際、RTTIとしてTypeInfo(動的配列型)も一緒に渡します。 …で、ここが肝ですが、Systemユニットの、Undocumentedなルーチンを用います。

それでは、可能な操作を列挙していきます。 上が普通のコード、下がマジックverです。

クリア

procedure Clear(var A: TStringDynArray);
begin
  A := nil
end;
procedure Clear(var A; TypeInfo: Pointer);
begin
  DynArrayClear(Pointer(A), TypeInfo)
end;

要素数取得

function Count(const A: TStringDynArray): Integer;
begin
  Result := Length(A)
end;
function Count(var A): Integer;
begin
  if Pointer(A) = nil then 
    Result := 0
  else
    Result := PCardinal(Cardinal(A) - SizeOf(LongInt))^
end;

長さ変更

procedure SetCount(var A: TStringDynArray; Value: Integer);
begin
  SetLength(A, Value)
end;
procedure SetCount(var A; TypeInfo: Pointer; Value: Integer);
begin
  DynArraySetLength(Pointer(A), TypeInfo, 1, @Value)
end;

UniqueString同様の処理

procedure Unique(var A: TStringDynArray);
begin
  A := Copy(A)
end;
procedure Unique(var A; TypeInfo: Pointer);
asm
  mov ecx, [eax]
  mov ecx, [ecx - 8]
  cmp ecx, 1
  jna @Exit
  mov ecx, eax
  mov eax, [eax]
  call System.@DynArrayCopy
@Exit:
end;

…いまいち、何が嬉しいんだ、って感じかもしれません。

それでは、実際に、Thebeで設定ファイルを読み書きしている箇所を披露しましょう。 バージョンは、alpha-20 prerelease 2003-11-29 です。

procedure SettingIO(Yaml: TYamlIO; var Setting: TSetting);

  procedure IOSetting; cdecl;

    procedure IOColors(Index: Integer); cdecl;
    var
      A: PColor;

      procedure IOColor; cdecl;
      begin
        Yaml.IO('name', A^.Name);
        Yaml.IO('color', A^.Color);
      end;

    begin
      A := @Setting.Colors[Index];
      Yaml.IO(CYIOHash(@IOColor), GetContext);
    end;

    procedure IOTypes(Index: Integer); cdecl;
    var
      A: PTypeSetting;

      procedure IOType; cdecl;

        procedure IORules(Index: Integer); cdecl;
        var
          B: PLexicalAnalyzingRule;

          procedure IORule; cdecl;
          begin
            Yaml.IO('from-state', B^.FromState);
            Yaml.IO('pattern', B^.Pattern);
            Yaml.IO('state', B^.State);
            Yaml.IO('to-state', B^.ToState);
          end;

        begin
          B := @A^.AnalyzingRules[Index];
          Yaml.IO(CYIOHash(@IORule), GetContext);
        end;

        procedure IOTemplates(Index: Integer); cdecl;
        var
          B: PTemplate;

          procedure IOTemplate; cdecl;
          begin
            Yaml.IO('accelerator', B^.Accelerator);
            Yaml.IO('text', B^.Text);
          end;

        begin
          B := @A^.Templates[Index];
          Yaml.IO(CYIOHash(@IOTemplate), GetContext);
        end;

      begin
        Yaml.IO('name', A^.Name);
        Yaml.IO('wild-card', A^.WildCard);
        Yaml.IO('command-line', A^.CommandLine);
        Yaml.IO('font-name', A^.FontName);
        Yaml.IO('font-size', A^.FontSize);
        Yaml.IO('half-width-font-name', A^.HalfWidthFontName);
        Yaml.IO('line-width', A^.LineWidth);
        Yaml.IO('tab-width', A^.TabWidth);
        Yaml.IO('hanging', A^.Hanging);
        Yaml.IO('expelling', A^.Expelling);
        Yaml.IO('word-wrap', A^.WordWrap);
        Yaml.IO('indenting', A^.Indenting);
        Yaml.IO('line-number', A^.LineNumber);
        Yaml.IO('line-margin', A^.LineMargin);
        Yaml.IO('analyzing-rules', A^.AnalyzingRules, TypeInfo(TLexicalAnalyzingRules), CYIOList(@IORules), GetContext);
        Yaml.IO('templates', A^.Templates, TypeInfo(TTemplateArray), CYIOList(@IOTemplates), GetContext);
      end;

    begin
      A := @Setting.Types[Index];
      Yaml.IO(CYIOHash(@IOType), GetContext);
    end;

  begin
    Yaml.IO('under-line', Setting.UnderLine);
    Yaml.IO('semi-free', Setting.SemiFree);
    Yaml.IO('setting-dialog-stay-on-top', Setting.SettingDialogStayOnTop);
    Yaml.IO('colors', Setting.Colors, TypeInfo(TColorArray), CYIOList(@IOColors), GetContext);
    Yaml.IO('types', Setting.Types, TypeInfo(TTypeSettingArray), CYIOList(@IOTypes), GetContext);
  end;

begin
  Yaml.IO(CYIOHash(@IOSetting), GetContext);
end;

これひとつで、読み込みと書き込みの両方を賄っています。 関数内関数へコールバックする技との併せ技で、見通し抜群と自画自賛しています。

TYamlIOの実装はここに置いて起きますので汚いコードに衝撃を受けてくださいませ。

YAMLの扱いそのものは、bogoYAML以下で…(…比較する事自体失礼ですね…ごめんなさい…)…機能限定版です。 ついでに、文字列のエスケープ方法が独自だったりして、純粋なサブセットですらありません。 (要するに、EOFile & Thebeの*.ini代わりであって、本物のYAMLを処理しようなんてことは、僕はこれっぽっちも思っていないのです)

ひとつのルーチンで読み書き両方のシリアライズを行う例は幾つかあるのですが、単にフィールド(メンバ変数と思ってください)を順番に読み書きするだけならともかく、動的配列(vectorと思ってください)が混じってくると話が単純にはいきません。 読み込みルーチンは、ひとつ読み込むごとに、要素数を増やさないといけないし、書き込みルーチンは、未知の配列型の要素数を取得できないといけないのです。

そこで動的配列アクロバットです。 いかがでしょう? 実用でしょ?

ま、C++ならtemplateでどうにでもなるでしょうけれど。 *2


*1 長い文字列と同じように実装されているくせに、コピーオンライトされない。

*2 しかしtemplateはコードを複写するので、これはRTTIの方がコードサイズを小さくできる例。