Наш японский коллега Jun Hosokawa заметил интересную особенность ограничения «constructor» в дженериках Delphi.
type
TConstructorConstraint<T: constructor> = class
end;
В качестве вступления цитата из справки:
This means that the actual argument type must be a class that defines a default constructor (a public parameterless constructor), so that methods within the generic type may construct instances of the argument type using the argument type’s default constructor, without knowing anything about the argument type itself (no minimum base type requirements).
То есть, согласно документации это ограничение позволяет убедиться, что аргумент дженерика — это класс, который имеет публичный конструктор без параметров, благодаря чему дженерик сможет создать экземпляр соответствующего типа, ничего о нем не зная. Здравый смысл может подсказывать нам, что речь только о классах, но на практике все оказывается не так просто. И встретив на просторах интернета высказывания о том, что это при использовании этого ограничения в качестве аргумента все-таки можно передавать не только классы, Jun решил проверить как такое возможно.
unit Unit1;
interface
type
// нет конструктора без параметров
TBar = class
public
constructor Create(const iDummy: Integer); reintroduce;
end;
procedure Test;
implementation
uses
System.Rtti;
type
TConstructorConstraint<T: constructor> = class
private
FValue: T;
public
constructor Create; reintroduce;
function ToString: string; override;
end;
// конструктор без параметров
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 !!!
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 + ' Managed;';
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;
// у этого конструктора есть параметры, он не сработает
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>;
Factor: TConstructorConstraint<TFactor>;
begin
Foo := nil;
Bar := nil;
Str := nil;
Int := nil;
Ary := nil;
Sets := nil;
Factor := 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;
Factor := TConstructorConstraint<TFactor>.Create;
WriteLn(Foo.ToString);
WriteLn(Bar.ToString);
WriteLn(Str.ToString);
WriteLn(Int.ToString);
WriteLn(Ary.ToString);
WriteLn(Sets.ToString);
WriteLn(Factor.ToString);
Readln;
finally
Factor.Free;
Sets.Free;
Ary.Free;
Int.Free;
Str.Free;
Bar.Free;
Foo.Free;
end;
end;
end.
Тип TFactor был объявлен, но автор не добавил его в тесты (видимо, забыл). Я это поправил.
Программа очень простая и всего-лишь выводит информацию о типе аргумента (спасибо, RTTI). Ниже результат её работы:
TFoo Created !
FValue: TFoo; Instance;
FValue: TBar; Public; Instance;
FValue: string; Public; Manged;
FValue: Integer; Public; Ordinal;
FValue: TStringDynArray; Managed;
FValue: TSet; Set;
FValue: TFactor; Ordinal;
И это удивительно, но это работает. Мы видим информацию о типах. Интересно, что TBar, в отличии от TFoo — это public type. Разница вызвана тем, что они объявлены в разных секциях модуля (interface и implementation). А перечислимый тип превратился в ordinal, что в принципе логически верно.
Но самое интересное то, что все это вообще работает, хотя у многих из этих типов совсем нет конструктора!
Если посмотреть на ассемблерный код, то видно, что здесь имеет место compiler magic.
// 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
// string
Project1.dpr.28: FValue := T.Create;
00407E22 8B45FC mov eax,[ebp-$04]
00407E25 83C004 add eax,$04
00407E28 E843DDFFFF call @UStrClr // код инициализации
// Integer
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
Видно, что при необходимости компилятор вместо конструктора использует код, выполняющий инициализацию экземпляра соответствующего типа.
Можно прийти к выводу, что нет особого смысла в использовании ограничения «constructor» отдельно от других ограничений, очень уж широко оно трактуется компилятором.
Чтобы ограничить аргументы дженерика именно классами, следует использовать следующий код:
type
TConstructorConstraint<T: class, constructor> = class
end;
И интересно, что если убрать из этого кода слово «constructor», то дженерик потеряет возможность создать экземпляр класса, так как именно слово «constructor» позволяет гарантировать, что аргумент будет не просто классом, но так же он будет иметь конструктор по умолчанию. Но ведь в Delphi все классы наследуются от TObject и тем самым всегда имеют публичный конструктор по умолчанию.
В чем же здесь смысл?
P.S. Благодарю сестру за помощью с переводом с японского.