Наш японский коллега 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. Благодарю сестру за помощью с переводом с японского.