LoginSignup
8
7

More than 1 year has passed since last update.

Delphi におけるジェネリックプログラミング

Last updated at Posted at 2022-03-24

はじめに

Delphi のジェネリックプログラミングに関する記事です。

ジェネリックプログラミングとは?

ジェネリックプログラミングを簡単に言うと、型をパラメータ化して型に依らない汎用的なコードを書く というものです。

例えば、A と B の変数の値を交換する Swap() という手続きがあるとしましょう。

procedure Swap(var A, B: Integer);
var
  C: Integer;
begin
  C := A;
  A := B;
  B := C;
end;

実数型と文字列型の変数も交換したくなりました。

procedure Swap(var A, B: Integer); overload;
procedure Swap(var A, B: Real); overload;
procedure Swap(var A, B: String); overload;

...

procedure Swap(var A, B: Integer);
var
  C: Integer;
begin
  C := A;
  A := B;
  B := C;
end;

procedure Swap(var A, B: Real);
var
  C: Real;
begin
  C := A;
  A := B;
  B := C;
end;

procedure Swap(var A, B: String);
var
  C: String;
begin
  C := A;
  A := B;
  B := C;
end;

日付時刻型の...と、際限がありません。それぞれの手続きは型にしか違いはないのですから、型をパラメータで仮置き (プレースホルダ) して、使う時に任意の型を指定できるようにすればいいのでは?というのがジェネリックプログラミングの考え方です。

誰ですか? 「インクルードファイルで書けらぁ!」 なんて言うのは。

GenericsTest2.dpr
program GenericsTest2;

{$APPTYPE CONSOLE}

uses
  SysUtils;

procedure Swap(var A, B: Integer); overload;
var
  C: Integer;
{$I Generic.inc}

procedure Swap(var A, B: Real); overload;
var
  C: Real;
{$I Generic.inc}

procedure Swap(var A, B: string); overload;
var
  C: string;
{$I Generic.inc}

begin


end.
Generic.inc
begin
  C := A;
  A := B;
  B := C;
end;

そういう話ではないんですよねぇ。

Delphi のジェネリックプログラミングの始まり

Delphi におけるジェネリックプログラミングは Delphi 2007 から行えるようになりました。Delphi 2007 とは言っても Delphi 2007 for .NET の方で、for Win32 の方の対応は Delphi 2009 からとなります。

Delphi に導入されたジェネリックプログラミングの手法は、Delphi が文法の一部を参考にしている Ada からでも、Pascal の後継言語である Modula-2 からでもなく、C# 2.0 のものを参考にしています。

個人的に Delphi の言語拡張は Pascal 派生言語から持ってくるのが原則だと思っていますが 1、ジェネリックプログラミングに関しては (Delphi for .NET があったからとはいえ) C# を参考にしたのは正解だったと思っています。

See also:

Delphi にジェネリックプログラミングは必須なのか?

Delphi は静的型付け言語の中でも ALGOL の系譜なので特に型にうるさく、ジェネリックプログラミングが可能だと嬉しい場面が多いのは間違いないと思います。

が、「そんなに型ごとに関数作る場面あるか?」 という疑問も浮かんでくると思います。先程の Swap() の例で言うと、Integer と String しか要らないのであれば、確かにコピペしてオーバーロードした方が簡単で早いですし。

ジェネリックプログラミングはプログラミングパラダイムの一種なので、無理に使う必要はないですが、知っておいて損はないと思います。

See also:

ジェネリックス

Delphi におけるジェネリックプログラミングは基本的に型ベースです。

■ クラスのジェネリック

Swap() メソッドをクラスメソッドとして実装してみます。

type
  TSwap<T> = class
  public
    class procedure Swap(var A, B: T);
  end;

{ TSwap<T> }

class procedure TSwap<T>.Swap(var A, B: T);
var
  C: T;
begin
  C := A;
  A := B;
  B := C;
end;

T というプレースホルダが型パラメータ (Type Parameter) です。型パラメータの名前は別に何でもいいのですが、慣習的に Type の頭文字である T が使われます。型パラメータを用いて定義された型はジェネリック (Generic) またはジェネリック型 (Type generic) と呼ばれます 2

使い方は次のようになります。

var
  a, b: Integer;
  c, d: string;
begin
  a := 3;
  b := 5;
  TSwap<Integer>.Swap(a, b);
  Writeln('a= ', a);
  Writeln('b= ', b);

  c := 'Hello,';
  d := 'world.';
  TSwap<string>.Swap(c, d);
  Writeln('c= ', c);
  Writeln('d= ', d);
end.

TSwap<Integer>.Swap(a, b); に指定した Integer型引数 (Type Argument) と呼びます。

<> で括られた型パラメータを型パラメータリスト (Type Parameter List) と呼び、必要に応じて複数の型パラメータをカンマ区切りで指定できます。

type
  TPair<TKey,TValue> = class
  ...

■ レコードのジェネリック

ジェネリックはレコード型でも作れます。

type
  TSwap<T> = record
  public
    class procedure Swap(var A, B: T); static;
  end;

{ TSwap<T> }

class procedure TSwap<T>.Swap(var A, B: T);
var
  C: T;
begin
  C := A;
  A := B;
  B := C;
end;

■ 配列のジェネリック

ジェネリックは配列でも作れます。最近の Delphi ですと、汎用動的配列が TArray<T> として定義されています。

System.pas
TArray<T> = array of T;

文字列の動的配列は次のように定義されています。

System.Types.pas
TStringDynArray = TArray<string>;

・ジェネリック配列の代入互換性

Delphi は Pascal 系の言語なので、変数の代入互換性は 名前等価 (Name Equivalence) となっています。名前等価での問題は配列の代入などで見る事ができます。

type
  TArr = array [1..10] of string; // ユーザー定義の型
var
  Arr1: TArr;
  Arr2: array [1..10] of string; // ユーザー定義の型
  Arr3: array [1..10] of string; // ユーザー定義の型
begin
  Arr1 := Arr2; // 名前等価なので、代入はできない。Arr1 と Arr2 は別の型とみなされる。
  Arr2 := Arr3; // 名前等価なので、代入はできない。Arr2 と Arr3 は別の型とみなされる。
end.

しかしながら、ジェネリック配列においては 構造等価 (Structual Equivalence) のような振る舞いを見せる事があります。

type
  TArr<T> = array [1..10] of T; // ユーザー定義の型
  TIntArr = TArr<Integer>;      // ユーザー定義の型 (?)
var
  Arr1: TArr<Integer>; // ユーザー定義の型 (?)
  Arr2: TIntArr;
  Arr3: TIntArr;
begin
  Arr1 := Arr2; // 代入可能。Arr1 と Arr2 は同じ型とみなされる。
  Arr2 := Arr3; // Arr1 と Arr2 は同じ型。
end.

ジェネリックと型パラメータを組み合わせたものを 生成型 (Constructed type) と呼び、生成型のうちすべての型パラメータが実際の型であるものを 閉じた生成型 (Closed constructed type)、プレースホルダが一つでも残っているものを 開いた生成型 (Open constructed type) と呼びます。

上記例の TArr<Integer> は閉じた生成型ですが、閉じた生成型では型をユーザー定義したとはみなされないようです。

See also:

■ その他のジェネリック

その他にジェネリックを定義できる型には次のようなものがあります。

  • 手続き型
  • メソッドポインタ
  • インターフェイス

レコードとクラスではジェネリックを定義できますが、オブジェクト型でジェネリックを定義する事はできません。

■ 型パラメータを使ったメソッド

型そのものではなく、メソッドで型パラメータを使う事ができます。型パラメータ使って宣言されたメソッドをパラメータ化メソッド (Parameterized Methods) と呼びます。パラメータ型だけではなく、結果型 (戻り値) にも型パラメータを使う事ができます。

type
  TSwap = record
  public
    class procedure Swap<T>(var A, B: T); static;
  end;

{ TSwap }

class procedure TSwap.Swap<T>(var A, B: T);
var
  C: T;
begin
  C := A;
  A := B;
  B := C;
end;

使い方もほぼ同じです。

var
  a, b: Integer;
  c, d: string;
begin
  a := 3;
  b := 5;
  TSwap.Swap<Integer>(a, b);
  Writeln('a= ', a);
  Writeln('b= ', b);

  c := 'Hello,';
  d := 'world.';
  TSwap.Swap<string>(c, d);
  Writeln('c= ', c);
  Writeln('d= ', d);
end.

・パラメータ化メソッドの型推論 (XE7 以前)

パラメータ化メソッドでは型推論 (Type Inference) が働くため、本来呼び出し時には型引数を指定しなくてもいいのですが、Delphi XE7 以前だとメソッドに変数パラメータ (var) または out パラメータが使われている場合に型推論が働きません。

program GenericsTest;
{$APPTYPE CONSOLE}
uses
  SysUtils, Types;

type
  TSwap = record
  public
    class procedure Swap<T>(var A, B: T); static; // A, B は変数パラメータ
  end;

{ TSwap }

class procedure TSwap.Swap<T>(var A, B: T);
var
  C: T;
begin
  C := A;
  A := B;
  B := C;
end;

var
  a, b: Integer;
  c, d: String;
begin
  a := 3;
  b := 5;
//TSwap.Swap<Integer>(a, b);
  TSwap.Swap(a, b); // E2033 変数実パラメータと変数仮パラメータとは同一の型でなければなりません
  Writeln('a= ', a);
  Writeln('b= ', b);

  c := 'Hello,';
  d := 'world.';
//TSwap.Swap<String>(c, d);
  TSwap.Swap(c, d); // E2033 変数実パラメータと変数仮パラメータとは同一の型でなければなりません
  Writeln('c= ', c);
  Writeln('d= ', d);
end.

エラーが出るバージョンでは、上記 Swap() メソッドのパラメータから var を抜くとコンパイルが通るようになります。もちろん Swap() メソッドは正しく動作しませんが。

Pascal 用語で "仮パラメータ" はメソッド宣言時のパラメータです (単に "パラメータ" とも)。"実パラメータ" はメソッド呼び出し時のパラメータです (単に "引数" とも)。

See also:

・タプル

var / out パラメータの型推論の問題が長らく解決しなかったのは、ひょっとすると「汎用タプルを実装する予定があったから」かもしれませんね。

パラメータが 2 個と 3 個のタプルは Malcolm Groves 氏が作ったものがあります。

Swap() をタプルで結果を返すように書き換えたものが次のコードとなります。

program GenericsTest;
{$APPTYPE CONSOLE}
uses
  SysUtils,
  Generics.Tuples in 'Generics.Tuples.pas';

type
  TSwap = record
  public
    class function Swap<T>(A, B: T): TTuple<T, T>; static;
  end;

{ TSwap }

class function TSwap.Swap<T>(A, B: T): TTuple<T, T>;
begin
  result := TTuple<T, T>.Create(B, A);
end;

var
  a, b: Integer;
  c, d: string;
  LTupleI : ITuple<Integer, Integer>;
  LTupleS : ITuple<string, string>;
begin
  a := 3;
  b := 5;
  LTupleI := TSwap.Swap<Integer>(a, b);
  Writeln('a= ', LTupleI.Value1);
  Writeln('b= ', LTupleI.Value2);

  c := 'Hello,';
  d := 'world.';
  LTupleS := TSwap.Swap<String>(c, d);
  Writeln('c= ', LTupleS.Value1);
  Writeln('d= ', LTupleS.Value2);
end.

パラメータの型が 2 つとも同じなので冗長なコードに見えますね。Swap() はタプルの例としては良くなかったかもしれません。単に複数の値を返したいのであれば、レコード型でいいのですから。

program GenericsTest;
{$APPTYPE CONSOLE}
uses
  SysUtils;

type
  TResult<T> = record
    Value1: T;
    Value2: T;
  end;

  TSwap = record
  public
    class function Swap<T>(A, B: T): TResult<T>; static;
  end;

{ TSwap }

class function TSwap.Swap<T>(A, B: T): TResult<T>;
begin
  result.Value1 := B;
  result.Value2 := A;
end;

var
  a, b: Integer;
  c, d: string;
  LResultI : TResult<Integer>;
  LResultS : TResult<string>;
begin
  a := 3;
  b := 5;
  LResultI := TSwap.Swap<Integer>(a, b);
  Writeln('a= ', LResultI.Value1);
  Writeln('b= ', LResultI.Value2);

  c := 'Hello,';
  d := 'world.';
  LResultS := TSwap.Swap<String>(c, d);
  Writeln('c= ', LResultS.Value1);
  Writeln('d= ', LResultS.Value2);
end.

Delphi 10.3 Rio 以降だと、パラメータ化メソッドで型引数を省略できる上、インライン型宣言と型推論も利用可能なので、もっとスッキリ書けます。

program GenericsTest;
{$APPTYPE CONSOLE}
uses
  SysUtils;

type
  TResult<T> = record
    Value1: T;
    Value2: T;
  end;

  TSwap = record
  public
    class function Swap<T>(A, B: T): TResult<T>; static;
  end;

{ TSwap }

class function TSwap.Swap<T>(A, B: T): TResult<T>;
begin
  result.Value1 := B;
  result.Value2 := A;
end;

begin
  var a := 3;
  var b := 5;
  var LResultI: TResult<Integer> := TSwap.Swap(a, b);
  Writeln('a= ', LResultI.Value1);
  Writeln('b= ', LResultI.Value2);

  var c := 'Hello,';
  var d := 'world.';
  var LResultS: TResult<string> := TSwap.Swap(c, d);
  Writeln('c= ', LResultS.Value1);
  Writeln('d= ', LResultS.Value2);
end.

今回の例の場合、そもそも 2 つの入力を 2 つで返す意味はないんですけどね。交換になってないし、このレコードを使う必要もありませんし。

program GenericsTest;
{$APPTYPE CONSOLE}
begin
  var a := 3;
  var b := 5;
  Writeln('a= ', b);
  Writeln('b= ', a);

  var c := 'Hello,';
  var d := 'world.';
  Writeln('c= ', d);
  Writeln('d= ', c);
end.

つまりはこれでいいじゃない、と (w

See also:

■ 型パラメータを使ったルーチン

Delphi では型パラメータを用いた手続きや関数 (ルーチン) を作る事はできません。似たような事をやりたいのであれば、レコードのパラメータ化 (クラス) メソッドとして実装するのが簡単だと思います。

フォームやデータモジュールがあるプロジェクトなら、それらのクラスにパラメータ化 (クラス) メソッドとして宣言すればいいので、そんなに困る制限ではないと思います。

See also:

・標準ジェネリック型ルーチン

先述の通り、型パラメータを使ったルーチンを記述する事はできませんが、標準ルーチンに ジェネリック型関数 というものが存在します。

ルーチン 説明
function Default(var X): <T>; 変数をその型のデフォルトで初期化します。
function GetTypeKind(T: TypeIdentifier): Integer; 指定された型から型の種類を返します。型の種類を知るだけなら TypeInfo() を使うより簡単です。
function HasWeakRef(T: TypeIdentifier): Boolean; ARC 対応コンパイラで、型が特定のメモリサポートを必要とする弱い参照を持っているかを返します。
XE6 以前では System.TypInfo.HasWeakRef(TypeInfo(T)) のようにする必要がありました。
function IsManagedType(T: TypeIdentifier): Boolean; 文字列または動的配列の型がインメモリで管理されているかを返します。
XE6 以前では System.Rtti.IsManaged(TypeInfo(T)) のようにする必要がありました。
function SizeOf(var X): Integer; 変数または型が占めるバイト数を返します。
function TypeInfo(T: TypeIdentifier): PTypeInfo; 指定された型の RTTI 情報を返します。

Delphi では Write() / Writeln() に使われている Write パラメータ を用いたルーチンを作る事はできませんし、同様に可変個パラメータのルーチンを作る事もできません。自己記述できないルーチンの多くは標準 Pascal や Turbo Pascal との互換性のために実装されており、極力実装しない方針となっているようです。

See also:

■ 制約

型パラメータには制約を付ける事ができます。例えば次のジェネリックはクラス型に限定されます。

type
  TFoo<T: class> = ...

次のジェネリックはコンポーネントクラスに限定されます。

type
  TBar<T: TComponent> = ...

制約に使える型は次の通りです。

  • インターフェイス型 (カンマ区切りで複数指定可能)
  • クラス型
  • 予約語 constructorclass、または record

制約を使う主な利点は、メソッド内のエラーチェック (型チェック) を省ける事です。

See also:

■ 型引数の型を知る

型毎の事情を考慮しなくていいジェネリックプログラミングですが、中にはどうしても型毎の個別処理を記述しなくてはならない場合があります。例えば型毎に存在するルーチンを一つにまとめたラッパーを作るような時です。

型の情報は TypeInfo() 関数で取得できます。この関数は TTypeInfo 構造体へのポインタを返します。

program GenericsTest;
{$APPTYPE CONSOLE}
uses
  SysUtils, TypInfo;

type
  TFoo = record
  public
    class procedure SetValue<T>(Value: T); static;
  end;

{ TFoo }

class procedure TFoo.SetValue<T>(Value: T);
var
  PTI: PTypeInfo;
begin
  PTI := TypeInfo(T);
  Writeln('Kind: ', Ord(PTI^.Kind));
  Writeln('Name: ', PTI^.Name);
end;

begin
  TFoo.SetValue<Integer>(123);   // Kind:1   Name: Integer
  TFoo.SetValue<Double>(3.14);   // Kind:4   Name: Double
  TFoo.SetValue<string>('ABC');  // Kind:18  Name: string
  TFoo.SetValue<TDateTime>(Now); // Kind:4   Name: TDateTime
end.

TTypeInfo.Kind で大まかな型の分類を、TTypeInfo.Name で型名を知る事ができます。

See also:

■ System.Generics.Collections

System.Generics.Collections には、汎用的なジェネリックスが定義されています。

従来のコンテナクラスのジェネリックバージョンもあります。

従来のコンテナ ジェネリックコンテナ
System.Classes.TList TList<T>
System.Contnrs.TStack TStack<T>
System.Contnrs.TQueue TQueue<T>
TDictionary<TKey, TValue>
System.Contnrs.TObjectList TObjectList<T: class>
System.Contnrs.TObjectStack TObjectStack<T: class>
System.Contnrs.TObjectQueue TObjectQueue<T: class>
TObjectDictionary<TKey,TValue>

See also:

・ジェネリック版 TList

TList は汎用的なリストであり、従来はリストにアイテムとしてレコードを追加するのに New() で動的変数を作成してそのポインタを渡す必要がありました。もちろん動的変数の廃棄も自前で行わなくてはなりませんでした。

TListTest1.dpr
program TListTest1;
{$APPTYPE CONSOLE}
uses
  System.SysUtils, System.Classes;

type
  TMyRec = record
    Code: Integer;
    Name: string;
  end;
  PMyRec = ^TMyRec;

begin
  ReportMemoryLeaksOnShutdown := True;
  var List := TList.Create; // 従来の TList
  try
    // Write
    var MyRec: PMyRec;
    for var i:=1 to 10 do
      begin
        New(MyRec);
        MyRec^.Code := i;
        MyRec^.Name := StringOfChar(Chr(Ord('A') + Pred(i)), 10);
        List.Add(MyRec);
      end;
    // Read
    for var MR in List do
      Writeln('Code=', PMyRec(MR)^.Code, ', ', 'Name=', PMyRec(MR)^.Name);
    // Release
    for var MR in List do
      Dispose(PMyRec(MR));
  finally
    List.Free;
  end;
end.

Dispose() で動的変数を廃棄しないとメモリリークします。
image.png
ジェネリックな TList<T> だと次のように書けます。

TListTest2.dpr
program TListTest2;
{$APPTYPE CONSOLE}
uses
  System.SysUtils, System.Generics.Collections;

type
  TMyRec = record
    Code: Integer;
    Name: string;
  end;

begin
  ReportMemoryLeaksOnShutdown := True;
  var List := TList<TMyRec>.Create; // ジェネリック版 TList
  try
    // Write
    var MyRec: TMyRec;
    for var i:=1 to 10 do
      begin
        MyRec.Code := i;
        MyRec.Name := StringOfChar(Chr(Ord('A') + Pred(i)), 10);
        List.Add(MyRec);
      end;
    // Read
    for var MR in List do
      Writeln('Code=', MR.Code, ', ', 'Name=', MR.Name);
  finally
    List.Free;
  end;
end.

リストには値として TMyRec を追加しているので、動的変数の作成も破棄も必要ありません。

See also:

■ Delphi における "ジェネリック" と "ジェネリックス" という用語の違い

Delphi では型パラメータを使って定義された型 (汎用体 / 総称型) の事をジェネリック (Generic) と呼び、ジェネリックとパラメータ化メソッドを総称してジェネリックス (Generics) と呼んでいるようです。

あと、細かい事ですが ジェネリクス ではなく ジェネリックスです (w 3

おわりに

Delphi におけるジェネリックプログラミングについてでした。

まだ追記するかもしれません。

  1. 文法的に相性がいいので。C# も後継言語と言えなくもない?

  2. 最初は 型パラメータ化型 (Type parameterized type) / パラメータ化型 (Parameterized type) と呼ばれていました。

  3. 『Object Pascal Handbook』では ジェネリクス なんだよなぁ...。

8
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
7