ジェネリック関数を作る。
作る(作れるとは言ってない)
現在のDelphiの仕様では、ジェネリクスを使用した処理を記述しようとした場合、クラス単位、あるいはクラスに属するメソッド単位でしか使用することはできません。つまり、ジェネリック手続きやジェネリック関数は作ることができません。
先日発表されたロードマップでは、Delphi 10.3において言語仕様の拡張が明記され、今後使えるようになる可能性は少しだけ見えてきました。しかし、例えば文字列を列挙型に変換する以下のような関数は今はまだ作ることができません。
program GenericFunction; uses Unit1; type TNumber = (One, Two, Three); var num: TNumber; begin num := StrToEnum<TNumber>('Two'); end.
作る(作れないとも言ってない)
しかし上記のコードを一切変えること無くコンパイルする方法があります。ジェネリック関数はもちろん作れないんですが、ジェネリック関数っぽい記述は実はできたりします。それを可能にするUnit1の中身は以下のようになります。
unit Unit1; interface uses System.TypInfo; type StrToEnum<T: record> = record private FValue: T; public class operator Explicit(const Value: string): StrToEnum<T>; class operator Implicit(const Value: StrToEnum<T>): T; inline; end; implementation class operator StrToEnum<T>.Explicit(const Value: string): StrToEnum<T>; var ret: Integer; begin if GetTypeKind(T) = tkEnumeration then begin ret := GetEnumValue(TypeInfo(T), Value); Move(ret, Result.FValue, SizeOf(T)); end else raise Exception.Create('Type parameter ''T'' must be a enumeration type'); end; class operator StrToEnum<T>.Implicit(const Value: StrToEnum<T>): T; begin Result := Value.FValue; end; end.
解説
蓋を開けてみればなんてことの無い、単なるキャストのオーバーロードです。文字列を一旦StrToEnum
論理上は2つのメソッドコールが発生していますが、片方にinlineが指定されているのでインライン展開されてひとつの呼び出しになり、速度的なデメリットはほぼないです。また、以前書いたようにGetTypeKindはコンパイル時に静的に解決されるため、実際に出来上がったEXEにはraise Exception.Create(...)の部分は生成されません。
余談
ちなみにこのExplicitとImplitcitを連携させる手法*1はジェネリクスじゃなくても通用するんですが、振る舞いだけを見るとC++のファンクタ(関数オブジェクト)のDelphi版と言えるかもしれません。もちろんインスタンス化できないので同等とまではとても言えませんが、class varでフィールドを用意すれば一応状態を持った関数と言えなくもないです。常に2つのメソッドを必要とするので実装はめんどくさいですが。
*1:ImplicitとImplicitでも動くけど