Swanman's Horizon

性欲をもてあましつつなんらかの話をするよ。

ジェネリック関数を作る。

作る(作れるとは言ってない)

現在の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型に「明示的に(Explicit)」キャストし、これをT型に代入する際に「暗黙的に(Implicit)」キャストが行われます。しかし使用時の記述だけを見てみれば、あたかもStrToEnumという関数を使用しているように見える…というトリックです。
論理上は2つのメソッドコールが発生していますが、片方にinlineが指定されているのでインライン展開されてひとつの呼び出しになり、速度的なデメリットはほぼないです。また、以前書いたようにGetTypeKindはコンパイル時に静的に解決されるため、実際に出来上がったEXEにはraise Exception.Create(...)の部分は生成されません。

余談

ちなみにこのExplicitとImplitcitを連携させる手法*1ジェネリクスじゃなくても通用するんですが、振る舞いだけを見るとC++のファンクタ(関数オブジェクト)のDelphi版と言えるかもしれません。もちろんインスタンス化できないので同等とまではとても言えませんが、class varでフィールドを用意すれば一応状態を持った関数と言えなくもないです。常に2つのメソッドを必要とするので実装はめんどくさいですが。

*1:ImplicitとImplicitでも動くけど