Наш японский коллега 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. Благодарю сестру за помощью с переводом с японского.
1. Станислав
9 Мар 2016 9:36 дп
Добрый день.
Огромное спасибо за статью! Бесценная информация о данном ограничении дженериков позволила взглянуть на архитектуру программы под другим углом, что, в свою очередь, сдвинуло процесс разработки с мёртвой (точнее, вялотекущей) точки :-) Вообщем — think different в действии!
С уважением, Станислав.
2. Pyh Tak
24 Мар 2016 3:06 пп
Добрый день
Что происходит здесь? —
Factor := TConstructorConstraint.Create;
Видимо Вы добавили это по ошибке (в оригинале этого нет)