2013年7月1日月曜日

constructor 制約について

Facebook の Delphi Talks で「型制約について、疑問があります」というスレッドが立ちました。
内容は「ジェネリクス型で型指定するときに、レコード型と値型と文字列型、のみ指定することは可能か?」という話でした。
これについては、スレッド中で「できない」と、結論が出ました。
ただ、このスレッド中で「constructor 制約がついたジェネリクス型に文字列型や値型が渡せる」という事が判りました(僕が知りました)。
と、いうことで、ちょっと調べてみました。

constructor 制約とは、下記のようにジェネリクスの型指定に constructor と書く事で定義される制約です。

type
TConstructorConstraint<T: constructor> = class
end;

具体的には、この制約を課されると「引数無しの Create を持った型しか指定できない」という制約です。
この制約の最大の特徴は T が 「どんな型か全く知らなくても」インスタンスを 生成できる、ということです。
これによって、何も知らなくても有効なインスタンスの存在を保証できます。

……と、まあ原義はさておき、上述の様にプリミティブ型である String 型や Integer 型を指定できるのです。
当然プリミティブな型なので Create なんてコンストラクタメソッドを持っているわけがありません。
これはどういった事でしょうか。
これを検証するために、下記のコードを書きました。

unit Unit1;
 
interface
 
type
// interface 部に定義すると全員に見える
// 引数無しの Create は持っていないクラス
TBar = class
public
constructor Create(const iDummy: Integer); reintroduce;
end;
 
procedure Test;
 
implementation
 
uses
System.Rtti;
 
type
// constructor 制約を課したジェネリック型クラス
TConstructorConstraint<T: constructor> = class
private
FValue: T;
public
constructor Create; reintroduce;
function ToString: String; override;
end;
 
// 引数無しの Create を持つクラス
TFoo = class
public
constructor Create; reintroduce;
end;
 
// 動的配列型
TStringDynArray = array of String;
 
// 集合型
TFactor = (Windows, MacOSX, Android, iOS, WindowsPhone);
TSet = set of TFactor;
 
{ TConstructorConstraint<T> }
 
constructor TConstructorConstraint<T>.Create;
begin
inherited Create;
 
// T の型は知らないけど Create を呼び出せる!
FValue := T.Create;
end;
 
// T の型情報を出力する
function TConstructorConstraint<T>.ToString: String;
var
Rtti: TRttiContext;
Field: TRttiField;
FieldType: TRttiType;
begin
Result := '';
 
Rtti := TRttiContext.Create;
try
Field := Rtti.GetType(ClassInfo).GetField('FValue');
FieldType := Field.FieldType;
 
Result := Field.Name + ': ' + FieldType.ToString + ';';
 
if (FieldType.IsPublicType) then
Result := Result + ' Public;';
 
if (FieldType.IsManaged) then
Result := Result + ' Manged;';
 
if (FieldType.IsInstance) then
Result := Result + ' Instance;';
 
if (FieldType.IsOrdinal) then
Result := Result + ' Ordinal;';
 
// Record は constructor 制約では指定できないので、ここは表示されない
if (FieldType.IsRecord) then
Result := Result + ' Record;';
 
if (FieldType.IsSet) then
Result := Result + ' Set;';
finally
Rtti.Free;
end;
end;
 
{ TFoo }
 
constructor TFoo.Create;
begin
inherited Create;
 
// 生成時に表示される
Writeln('TFoo Created !');
end;
 
{ TBar }
 
constructor TBar.Create(const iDummy: Integer);
begin
inherited Create;
 
// 引数無しの Create ではないため、表示されない
Writeln('TBar Created !');
end;
 
// 生成して情報を出力する
procedure Test;
var
Foo: TConstructorConstraint<TFoo>;
Bar: TConstructorConstraint<TBar>;
Str: TConstructorConstraint<String>;
Int: TConstructorConstraint<Integer>;
Ary: TConstructorConstraint<TStringDynArray>;
Sets: TConstructorConstraint<TSet>;
begin
Foo := nil;
Bar := nil;
Str := nil;
Int := nil;
Ary := nil;
Sets := nil;
try
Foo := TConstructorConstraint<TFoo>.Create;
Bar := TConstructorConstraint<TBar>.Create;
Str := TConstructorConstraint<String>.Create;
Int := TConstructorConstraint<Integer>.Create;
Ary := TConstructorConstraint<TStringDynArray>.Create;
Sets := TConstructorConstraint<TSet>.Create;
 
Writeln(Foo.ToString);
Writeln(Bar.ToString);
Writeln(Str.ToString);
Writeln(Int.ToString);
Writeln(Ary.ToString);
Writeln(Sets.ToString);
 
Readln;
finally
Sets.Free;
Ary.Free;
Int.Free;
Str.Free;
Bar.Free;
Foo.Free;
end;
end;
 
end.

様々なジェネリック型を定義して、生成、その情報を出力、とするだけのプログラムです。
ここでは、クラス型、文字列型、整数型、動的配列型、集合型、を指定してみました。
重要なのは、クラス型以外、Create なんてメソッドは持っていない!ということです。

では、このプログラムを動かしてみると…

TFoo Created !
FValue: TFoo; Instance;
FValue: TBar; Public; Instance;
FValue: string; Public; Manged;
FValue: Integer; Public; Ordinal;
FValue: TStringDynArray; Manged;
FValue: TSet; Set;

こんな風になりました!
TFoo, TBar はクラスなので "Instance" と表示されています。
また、TBar は、interface 部で定義されているので公開されている型 "Public" と表示されています。
そして、string と動的配列型は "Managed" と表示されています。これはコンパイラがその型の生成と廃棄を担っていることを示します。
つまり、string と動的配列型は、本当は管理されたメモリを持つ参照型であるため、このように表示されています。
また、Integer は Ordinal…順序型, 集合型は Set と出ました。これはそれぞれの型そのものですね。

ということで、本当に値型や文字列型が Create というメソッドで生成されてしまいました。
本来、そのようなメソッドを持たない型にも関わらず、です。

では、これらの型の生成は実際にはどのようなコードになっているのでしょうか?
それを見るために CPU ビューで逆アセンブルされたコードを見てみました。

// TFoo の生成
Project1.dpr.28: FValue := T.Create;
00407D7A B201             mov dl,$01
00407D7C A158724000       mov eax,[$00407258]
00407D81 E85AF6FFFF       call TFoo.Create      // 初期化コード
00407D86 8B55FC           mov edx,[ebp-$04]
00407D89 894204           mov [edx+$04],eax


// TBar の生成
Project1.dpr.28: FValue := T.Create;
00407DCE B201             mov dl,$01
00407DD0 A114734000       mov eax,[$00407314]
00407DD5 E8BACDFFFF       call TObject.Create   // 初期化コード
00407DDA 8B55FC           mov edx,[ebp-$04]
00407DDD 894204           mov [edx+$04],eax


// 文字列の生成
Project1.dpr.28: FValue := T.Create;
00407E22 8B45FC           mov eax,[ebp-$04]
00407E25 83C004           add eax,$04
00407E28 E843DDFFFF       call @UStrClr         // 初期化コード


// 整数型(順序型)の生成
Project1.dpr.28: FValue := T.Create;
00407E6E 8B45FC           mov eax,[ebp-$04]
00407E71 33D2             xor edx,edx           // 初期化コード
00407E73 895004           mov [eax+$04],edx


// 動的配列の生成
Unit1.pas.50: FValue := T.Create;
004D50F6 8B45FC           mov eax,[ebp-$04]
004D50F9 83C004           add eax,$04
004D50FC 8B1500394D00     mov edx,[$004d3900]
004D5102 E87D68F3FF       call @DynArrayClear   // 初期化コード


// 集合の生成
Unit1.pas.50: FValue := T.Create;
004D540E 8B45FC           mov eax,[ebp-$04]
004D5411 8A153C544D00     mov dl,[$004d543c]    // 初期化コード?
004D5417 885004           mov [eax+$04],dl

なんと、コンパイラマジックによって自動的にそれぞれの型の初期化コードが走っていました。
文字列や動的配列であれば、それらの内容をクリアする関数をコールしています。
順序型は 0 が代入されています(同じレジスタを xor するとレジスタの内容は 0 になる)。
集合型については、ちょっと判りませんが……
また、TFoo, TBaz については、それぞれ「引数無しの Create」が呼ばれています。
TFoo は定義されているので良いですが TBaz の場合基底クラスである TObject の Create が呼ばれました。
クラス型は全て TObject から派生しているので、constructor で制限を掛けても全てのクラスが生成できてしまいます。その Create が有効如何に関わらずです。
これは、constructor 制約の意義に関わる注意点です……実質意味が無い気がします……

ということで、 constructor 制約で実際に生成されるコードを見てみました。
結論としては、コンパイラが上手いことやってくれてる、っていうだけのお話でした。

ちなみに <T: class, constructor> というように class の制限も一緒につけると値型や文字列型は指定できなくなります。
また、<T: record, constructor> とすると、プリミティブ型しか指定できなくなります。つまり、クラス型は指定できません。

constructor 制約に値型や文字列型がわたせるのは、上記の "class", "record" のどちらも指定していないため、どっちも通るよ!っていう事なのだと思います。

それと、軽く流してしまいましたがジェネリックで指定された型が何型なのかは拡張 RTTI メソッド群を使えば取得できます。
詳しくは、上記のコード中の TConstructorConstrain クラスの ToString メソッドを参照してください。