はじめに
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;
日付時刻型の...と、際限がありません。それぞれの手続きは型にしか違いはないのですから、型をパラメータで仮置き (プレースホルダ) して、使う時に任意の型を指定できるようにすればいいのでは?というのがジェネリックプログラミングの考え方です。
誰ですか? 「インクルードファイルで書けらぁ!」 なんて言うのは。
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.
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>
として定義されています。
TArray<T> = array of T;
文字列の動的配列は次のように定義されています。
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:
- (6.4.4) 構造等価 (Structual Equivalence) と 名前等価 (Name Equivalence) (Qiita)
- ジェネリックスでのオーバーロードおよび型の互換性 (DocWiki)
■ その他のジェネリック
その他にジェネリックを定義できる型には次のようなものがあります。
- 手続き型
- メソッドポインタ
- インターフェイス
レコードとクラスではジェネリックを定義できますが、オブジェクト型でジェネリックを定義する事はできません。
■ 型パラメータを使ったメソッド
型そのものではなく、メソッドで型パラメータを使う事ができます。型パラメータ使って宣言されたメソッドをパラメータ化メソッド (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:
- 標準ルーチンと入出力 (DocWiki)
- GetTypeKind を使って擬似的に細かい型制約のジェネリクスを実現する。(Swanman's Horizon)
- (6.1.4.) 配列定数とグローバル配列変数の初期化 (Qiita)
■ 制約
型パラメータには制約を付ける事ができます。例えば次のジェネリックはクラス型に限定されます。
type
TFoo<T: class> = ...
次のジェネリックはコンポーネントクラスに限定されます。
type
TBar<T: TComponent> = ...
制約に使える型は次の通りです。
- インターフェイス型 (カンマ区切りで複数指定可能)
- クラス型
- 予約語 constructor、class、または 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
には、汎用的なジェネリックスが定義されています。
従来のコンテナクラスのジェネリックバージョンもあります。
See also:
・ジェネリック版 TList
TList
は汎用的なリストであり、従来はリストにアイテムとしてレコードを追加するのに New()
で動的変数を作成してそのポインタを渡す必要がありました。もちろん動的変数の廃棄も自前で行わなくてはなりませんでした。
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()
で動的変数を廃棄しないとメモリリークします。
ジェネリックな TList<T>
だと次のように書けます。
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 におけるジェネリックプログラミングについてでした。
まだ追記するかもしれません。