# Delphi におけるジェネリックプログラミング --- tags: Delphi programming Pascal embarcadero objectpascal created_at: 2022-03-24 updated_at: 2022-03-28 --- # はじめに Delphi のジェネリックプログラミングに関する記事です。 - [ジェネリックス (DocWiki)](https://docwiki.embarcadero.com/RADStudio/Alexandria/ja/%E3%82%B8%E3%82%A7%E3%83%8D%E3%83%AA%E3%83%83%E3%82%AF%E3%82%B9%EF%BC%9A%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9) - [ジェネリックプログラミング (Wikipedia)](https://ja.wikipedia.org/wiki/%E3%82%B8%E3%82%A7%E3%83%8D%E3%83%AA%E3%83%83%E3%82%AF%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0) ## ジェネリックプログラミングとは? **ジェネリックプログラミング**を簡単に言うと、`型をパラメータ化して型に依らない汎用的なコードを書く` というものです。 例えば、A と B の変数の値を交換する `Swap()` という手続きがあるとしましょう。 ```pascal procedure Swap(var A, B: Integer); var C: Integer; begin C := A; A := B; B := C; end; ``` 実数型と文字列型の変数も交換したくなりました。 ```pascal 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; ``` 日付時刻型の...と、際限がありません。それぞれの手続きは型にしか違いはないのですから、型をパラメータで仮置き (プレースホルダ) して、使う時に任意の型を指定できるようにすればいいのでは?というのがジェネリックプログラミングの考え方です。 誰ですか? **「インクルードファイルで書けらぁ!」** なんて言うのは。 ```pascal: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. ``` ```pascal: 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** のものを参考にしています。 :::note info 個人的に Delphi の言語拡張は Pascal 派生言語から持ってくるのが原則だと思っていますが [^1]、ジェネリックプログラミングに関しては (Delphi for .NET があったからとはいえ) **C#** を参考にしたのは正解だったと思っています。 ::: **See also:** - [Ada (Wikipedia)](https://ja.wikipedia.org/wiki/Ada) - [Modula-2 (Wikipedia)](https://ja.wikipedia.org/wiki/Modula-2) - [C# (Wikipedia)](https://ja.wikipedia.org/wiki/C_Sharp) ## Delphi にジェネリックプログラミングは必須なのか? Delphi は静的型付け言語の中でも **ALGOL** の系譜なので特に型にうるさく、ジェネリックプログラミングが可能だと嬉しい場面が多いのは間違いないと思います。 が、**「そんなに型ごとに関数作る場面あるか?」** という疑問も浮かんでくると思います。先程の `Swap()` の例で言うと、Integer と String しか要らないのであれば、確かにコピペしてオーバーロードした方が簡単で早いですし。 ジェネリックプログラミングは[プログラミングパラダイム](https://ja.wikipedia.org/wiki/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E3%83%91%E3%83%A9%E3%83%80%E3%82%A4%E3%83%A0)の一種なので、無理に使う必要はないですが、知っておいて損はないと思います。 **See also:** - [ALGOL (Wikipedia)](https://ja.wikipedia.org/wiki/ALGOL) # ジェネリックス Delphi におけるジェネリックプログラミングは基本的に型ベースです。 ## ■ クラスのジェネリック `Swap()` メソッドをクラスメソッドとして実装してみます。 ```pascal type TSwap = class public class procedure Swap(var A, B: T); end; { TSwap } class procedure TSwap.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]。 使い方は次のようになります。 ```pascal var a, b: Integer; c, d: string; begin a := 3; b := 5; TSwap.Swap(a, b); Writeln('a= ', a); Writeln('b= ', b); c := 'Hello,'; d := 'world.'; TSwap.Swap(c, d); Writeln('c= ', c); Writeln('d= ', d); end. ``` `TSwap.Swap(a, b);` に指定した `Integer` を**型引数** (Type Argument) と呼びます。 `<>` で括られた型パラメータを**型パラメータリスト** (Type Parameter List) と呼び、必要に応じて複数の型パラメータをカンマ区切りで指定できます。 ```pascal type TPair = class ... ``` ## ■ レコードのジェネリック ジェネリックはレコード型でも作れます。 ```pascal type TSwap = record public class procedure Swap(var A, B: T); static; end; { TSwap } class procedure TSwap.Swap(var A, B: T); var C: T; begin C := A; A := B; B := C; end; ``` ## ■ 配列のジェネリック ジェネリックは配列でも作れます。最近の Delphi ですと、汎用動的配列が `TArray` として定義されています。 ```pascal:System.pas TArray = array of T; ``` 文字列の動的配列は次のように定義されています。 ```pascal:System.Types.pas TStringDynArray = TArray; ``` ### ・ジェネリック配列の代入互換性 Delphi は Pascal 系の言語なので、変数の代入互換性は **名前等価** (Name Equivalence) となっています。名前等価での問題は配列の代入などで見る事ができます。 ```pascal 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) のような振る舞いを見せる事があります。 ```pascal type TArr = array [1..10] of T; // ユーザー定義の型 TIntArr = TArr; // ユーザー定義の型 (?) var Arr1: TArr; // ユーザー定義の型 (?) Arr2: TIntArr; Arr3: TIntArr; begin Arr1 := Arr2; // 代入可能。Arr1 と Arr2 は同じ型とみなされる。 Arr2 := Arr3; // Arr1 と Arr2 は同じ型。 end. ``` ジェネリックと型パラメータを組み合わせたものを **生成型** (Constructed type) と呼び、生成型のうちすべての型パラメータが実際の型であるものを **閉じた生成型** (Closed constructed type)、プレースホルダが一つでも残っているものを **開いた生成型** (Open constructed type) と呼びます。 上記例の `TArr` は閉じた生成型ですが、閉じた生成型では型をユーザー定義したとはみなされないようです。 **See also:** - [(6.4.4) 構造等価 (Structual Equivalence) と 名前等価 (Name Equivalence) (Qiita)](./eedda6d38b6d0887d4ac.md#644-%E6%A7%8B%E9%80%A0%E7%AD%89%E4%BE%A1-structual-equivalence-%E3%81%A8-%E5%90%8D%E5%89%8D%E7%AD%89%E4%BE%A1-name-equivalence) - [ジェネリックスでのオーバーロードおよび型の互換性 (DocWiki)](https://docwiki.embarcadero.com/RADStudio/ja/%E3%82%B8%E3%82%A7%E3%83%8D%E3%83%AA%E3%83%83%E3%82%AF%E3%82%B9%E3%81%A7%E3%81%AE%E3%82%AA%E3%83%BC%E3%83%90%E3%83%BC%E3%83%AD%E3%83%BC%E3%83%89%E3%81%8A%E3%82%88%E3%81%B3%E5%9E%8B%E3%81%AE%E4%BA%92%E6%8F%9B%E6%80%A7) ## ■ その他のジェネリック その他にジェネリックを定義できる型には次のようなものがあります。 - 手続き型 - メソッドポインタ - インターフェイス レコードとクラスではジェネリックを定義できますが、[オブジェクト型](./28fc0af9fc9e34a8d5c5.md#61-delphi-%E3%81%AE%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E5%9E%8B)でジェネリックを定義する事はできません。 ## ■ 型パラメータを使ったメソッド 型そのものではなく、メソッドで型パラメータを使う事ができます。型パラメータ使って宣言されたメソッドを**パラメータ化メソッド** (Parameterized Methods) と呼びます。パラメータ型だけではなく、結果型 (戻り値) にも型パラメータを使う事ができます。 ```pascal type TSwap = record public class procedure Swap(var A, B: T); static; end; { TSwap } class procedure TSwap.Swap(var A, B: T); var C: T; begin C := A; A := B; B := C; end; ``` 使い方もほぼ同じです。 ```pascal var a, b: Integer; c, d: string; begin a := 3; b := 5; TSwap.Swap(a, b); Writeln('a= ', a); Writeln('b= ', b); c := 'Hello,'; d := 'world.'; TSwap.Swap(c, d); Writeln('c= ', c); Writeln('d= ', d); end. ``` ### ・パラメータ化メソッドの型推論 (XE7 以前) パラメータ化メソッドでは**型推論** (Type Inference) が働くため、本来呼び出し時には型引数を指定しなくてもいいのですが、**Delphi XE7** 以前だとメソッドに変数パラメータ (**var**) または **out** パラメータが使われている場合に型推論が働きません。 ```pascal program GenericsTest; {$APPTYPE CONSOLE} uses SysUtils, Types; type TSwap = record public class procedure Swap(var A, B: T); static; // A, B は変数パラメータ end; { TSwap } class procedure TSwap.Swap(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(a, b); TSwap.Swap(a, b); // E2033 変数実パラメータと変数仮パラメータとは同一の型でなければなりません Writeln('a= ', a); Writeln('b= ', b); c := 'Hello,'; d := 'world.'; //TSwap.Swap(c, d); TSwap.Swap(c, d); // E2033 変数実パラメータと変数仮パラメータとは同一の型でなければなりません Writeln('c= ', c); Writeln('d= ', d); end. ``` エラーが出るバージョンでは、上記 `Swap()` メソッドのパラメータから **var** を抜くとコンパイルが通るようになります。もちろん `Swap()` メソッドは正しく動作しませんが。 :::note info Pascal 用語で "仮パラメータ" はメソッド宣言時のパラメータです (単に "パラメータ" とも)。"実パラメータ" はメソッド呼び出し時のパラメータです (単に "引数" とも)。 ::: **See also:** - [QC#78103: type inference not working for generic var and out parameters (Quality Central)](http://qc.embarcadero.com/wc/qcmain.aspx?d=78103) ### ・タプル **var** / **out** パラメータの型推論の問題が長らく解決しなかったのは、ひょっとすると「汎用**タプル**を実装する予定があったから」かもしれませんね。 パラメータが 2 個と 3 個のタプルは Malcolm Groves 氏が作ったものがあります。 - [generics.tuples (Github)](https://github.com/malcolmgroves/generics.tuples) `Swap()` をタプルで結果を返すように書き換えたものが次のコードとなります。 ```pascal program GenericsTest; {$APPTYPE CONSOLE} uses SysUtils, Generics.Tuples in 'Generics.Tuples.pas'; type TSwap = record public class function Swap(A, B: T): TTuple; static; end; { TSwap } class function TSwap.Swap(A, B: T): TTuple; begin result := TTuple.Create(B, A); end; var a, b: Integer; c, d: string; LTupleI : ITuple; LTupleS : ITuple; begin a := 3; b := 5; LTupleI := TSwap.Swap(a, b); Writeln('a= ', LTupleI.Value1); Writeln('b= ', LTupleI.Value2); c := 'Hello,'; d := 'world.'; LTupleS := TSwap.Swap(c, d); Writeln('c= ', LTupleS.Value1); Writeln('d= ', LTupleS.Value2); end. ``` パラメータの型が 2 つとも同じなので冗長なコードに見えますね。`Swap()` はタプルの例としては良くなかったかもしれません。単に複数の値を返したいのであれば、レコード型でいいのですから。 ```pascal program GenericsTest; {$APPTYPE CONSOLE} uses SysUtils; type TResult = record Value1: T; Value2: T; end; TSwap = record public class function Swap(A, B: T): TResult; static; end; { TSwap } class function TSwap.Swap(A, B: T): TResult; begin result.Value1 := B; result.Value2 := A; end; var a, b: Integer; c, d: string; LResultI : TResult; LResultS : TResult; begin a := 3; b := 5; LResultI := TSwap.Swap(a, b); Writeln('a= ', LResultI.Value1); Writeln('b= ', LResultI.Value2); c := 'Hello,'; d := 'world.'; LResultS := TSwap.Swap(c, d); Writeln('c= ', LResultS.Value1); Writeln('d= ', LResultS.Value2); end. ``` **Delphi 10.3 Rio** 以降だと、パラメータ化メソッドで型引数を省略できる上、インライン型宣言と型推論も利用可能なので、もっとスッキリ書けます。 ```pascal program GenericsTest; {$APPTYPE CONSOLE} uses SysUtils; type TResult = record Value1: T; Value2: T; end; TSwap = record public class function Swap(A, B: T): TResult; static; end; { TSwap } class function TSwap.Swap(A, B: T): TResult; begin result.Value1 := B; result.Value2 := A; end; begin var a := 3; var b := 5; var LResultI: TResult := TSwap.Swap(a, b); Writeln('a= ', LResultI.Value1); Writeln('b= ', LResultI.Value2); var c := 'Hello,'; var d := 'world.'; var LResultS: TResult := TSwap.Swap(c, d); Writeln('c= ', LResultS.Value1); Writeln('d= ', LResultS.Value2); end. ``` 今回の例の場合、そもそも 2 つの入力を 2 つで返す意味はないんですけどね。交換になってないし、このレコードを使う必要もありませんし。 ```pascal 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:** - [タプル (Wikipedia)](https://ja.wikipedia.org/wiki/%E3%82%BF%E3%83%97%E3%83%AB) ## ■ 型パラメータを使ったルーチン Delphi では型パラメータを用いた手続きや関数 (ルーチン) を作る事はできません。似たような事をやりたいのであれば、レコードのパラメータ化 (クラス) メソッドとして実装するのが簡単だと思います。 フォームやデータモジュールがあるプロジェクトなら、それらのクラスにパラメータ化 (クラス) メソッドとして宣言すればいいので、そんなに困る制限ではないと思います。 **See also:** - [ジェネリック関数を作る。(Swanman's Horizon)](https://lyna.hateblo.jp/entry/20160812/1470932247) ## ■ 制約 型パラメータには制約を付ける事ができます。例えば次のジェネリックはクラス型に限定されます。 ```pascal type TFoo = ... ``` 次のジェネリックはコンポーネントクラスに限定されます。 ```pascal type TBar = ... ``` 制約に使える型は次の通りです。 - インターフェイス型 (カンマ区切りで複数指定可能) - クラス型 - 予約語 **constructor**、**class**、または **record** 制約を使う主な利点は、メソッド内のエラーチェック (型チェック) を省ける事です。 **See also:** - [ジェネリックスの制約 (DocWiki)](https://docwiki.embarcadero.com/RADStudio/ja/%E3%82%B8%E3%82%A7%E3%83%8D%E3%83%AA%E3%83%83%E3%82%AF%E3%82%B9%E3%81%AE%E5%88%B6%E7%B4%84) ## ■ 型引数の型を知る 型毎の事情を考慮しなくていいジェネリックプログラミングですが、中にはどうしても型毎の個別処理を記述しなくてはならない場合があります。例えば型毎に存在するルーチンを一つにまとめたラッパーを作るような時です。 型の情報は `TypeInfo()` 関数で取得できます。この関数は `TTypeInfo` 構造体へのポインタを返します。 ```pascal program GenericsTest; {$APPTYPE CONSOLE} uses SysUtils, TypInfo; type TFoo = record public class procedure SetValue(Value: T); static; end; { TFoo } class procedure TFoo.SetValue(Value: T); var PTI: PTypeInfo; begin PTI := TypeInfo(T); Writeln('Kind: ', Ord(PTI^.Kind)); Writeln('Name: ', PTI^.Name); end; begin TFoo.SetValue(123); // Kind:1 Name: Integer TFoo.SetValue(3.14); // Kind:4 Name: Double TFoo.SetValue('ABC'); // Kind:18 Name: string TFoo.SetValue(Now); // Kind:4 Name: TDateTime end. ``` `TTypeInfo.Kind` で大まかな型の分類を、`TTypeInfo.Name` で型名を知る事ができます。 **See also:** - [System.TypeInfo (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/System.TypeInfo) - [System.TypInfo.TTypeInfo (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/System.TypInfo.TTypeInfo) - [System.TTypeKind (DocWiki)](https://docwiki.embarcadero.com/Libraries/Sydney/ja/System.TTypeKind) ## ■ System.Generics.Collections `System.Generics.Collections` には、汎用的なジェネリックスが定義されています。 **See also:** - [System.Generics.Collections (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/System.Generics.Collections) ## ■ Delphi における "ジェネリック" と "ジェネリックス" という用語の違い Delphi では型パラメータを使って定義された型 (汎用体 / 総称型) の事を**ジェネリック** (Generic) と呼び、ジェネリックとパラメータ化メソッドを総称して**ジェネリックス** (Generics) と呼んでいるようです。 あと、細かい事ですが **ジェネリクス** ではなく **ジェネリックス**です (w [^3] # おわりに Delphi におけるジェネリックプログラミングについてでした。 :::note info そのうち何か追記するかもしれません。 ::: [^1]: 文法的に相性がいいので。C# も後継言語と言えなくもない? [^2]: 最初は `型パラメータ化型 (Type parameterized type)` / `パラメータ化型 (Parameterized type)` と呼ばれていました。 [^3]: 『Object Pascal Handbook』では `ジェネリクス` なんだよなぁ...。