Delphi - странное поведение с конструкторами умных указателей

Я работаю над проектом, содержащим несколько пакетов. В одном из моих базовых пакетов я объявляю такой умный указатель (вот полный код):

unit UTWSmartPointer;

interface

type
    IWSmartPointer<T> = reference to function: T;

    TWSmartPointer<T: class, constructor> = class(TInterfacedObject, IWSmartPointer<T>)
    private
        m_pInstance: T;

    public
        constructor Create; overload; virtual;

        constructor Create(pInstance: T); overload; virtual;

        destructor Destroy; override;

        function Invoke: T; virtual;
    end;

implementation
//---------------------------------------------------------------------------
constructor TWSmartPointer<T>.Create;
begin
    inherited Create;

    m_pInstance := T.Create;
end;
//---------------------------------------------------------------------------
constructor TWSmartPointer<T>.Create(pInstance: T);
begin
    inherited Create;

    m_pInstance := pInstance;
end;
//---------------------------------------------------------------------------
destructor TWSmartPointer<T>.Destroy;
begin
    m_pInstance.Free;
    m_pInstance := nil;

    inherited Destroy;
end;
//---------------------------------------------------------------------------
function TWSmartPointer<T>.Invoke: T;
begin
    Result := m_pInstance;
end;
//---------------------------------------------------------------------------

end.

Позже в моем проекте (и в другом пакете) я использую этот умный указатель с объектом GDI + (TGpGraphicsPath). Объявляю графический путь так:

...
pGraphicsPath: IWSmartPointer<TGpGraphicsPath>;
...
pGraphicsPath := TWSmartPointer<TGpGraphicsPath>.Create();
...

Однако, когда я выполняю код, на экране ничего не отображается. Я не получаю ни ошибки, ни исключения, ни нарушения прав доступа, просто пустая страница. Но если я просто изменю свой код вот так:

...
pGraphicsPath: IWSmartPointer<TGpGraphicsPath>;
...
pGraphicsPath := TWSmartPointer<TGpGraphicsPath>.Create(TGpGraphicsPath.Create);
...

потом все стало нормально, и мой путь нарисован именно так, как положено. Но я не могу понять, почему первый конструктор не работает должным образом. Кто-нибудь может объяснить мне это странное поведение?

С Уважением


person Jean-Milost Reymond    schedule 15.02.2017    source источник
comment
Я предполагаю, что TGpGraphicsPath принимает необязательный аргумент, и в этом случае будет вызываться TObject.Create вместо TGpGraphicsPath.Create (optionalArgument) - то, чего вы должны остерегаться с дженериками.   -  person Dsm    schedule 15.02.2017
comment
@Remy Оказывается, анонимные методы на самом деле являются интерфейсами, и эта деталь реализации широко используется таким образом: stackoverflow.com/a/39955320 < / а>   -  person David Heffernan    schedule 15.02.2017
comment
@Dsm TGpGraphicsPath не имеет параметра в конструкторе.   -  person Remy Lebeau    schedule 15.02.2017
comment
Это настоящий код? IWSmartPointer не объявлен как interface, поэтому его нельзя использовать в объявлении class(). И не имеет смысла назначать указатель объекта на reference to function. Кроме того, Invoke() неправильно назван, так как он просто возвращает указатель на управляемый объект, но на самом деле ничего не вызывает. Я бы переименовал его в GetObject() или аналогичный, а затем определил бы property для его вызова.   -  person Remy Lebeau    schedule 15.02.2017
comment
@RemyLebeau Все конструкторы, объявленные TGpGraphicsPath, имеют параметры   -  person David Heffernan    schedule 15.02.2017
comment
@RemyLebeau Да, это настоящий код. Он компилируется. Компилятор принимает анонимный тип метода вместо интерфейса из-за упомянутых мною деталей реализации. Invoke назван правильно. Это имя единственного метода интерфейса анонимного метода. Это метод, который вызывается при вызове метода anon. Здесь вы упускаете некоторые важные подробности, и пока вы не поймете, я не уверен, что такие комментарии очень полезны.   -  person David Heffernan    schedule 15.02.2017
comment
@DavidHeffernan Я хорошо знаю, как анонимные типы методов реализуются под капотом, большое вам спасибо. Это деталь реализации. Анонимный тип случается реализуется с использованием интерфейса, но это не означает, что законно писать класс, который реализует анонимный тип. Класс может быть производным только от другого класса и может реализовывать только интерфейсы. Это договор. В контракте не говорится, что анонимный тип является интерфейсом, просто случается использовать интерфейс в текущей реализации, но они могут решить когда-нибудь изменить эту реализацию.   -  person Remy Lebeau    schedule 15.02.2017
comment
@remy Я понимаю. Я думал, вы предполагаете, что компилятор отклонит код в вопросе. И я согласен с тем, что использование подобных деталей реализации настраивает вас на падение.   -  person David Heffernan    schedule 15.02.2017
comment
@DavidHeffernan: на самом деле, я ожидал, что компиляция не удастся.   -  person Remy Lebeau    schedule 16.02.2017
comment
Спасибо всем за очень интересные ответы. Фактически, поскольку я новичок в Delphi, я основал свою реализацию интеллектуального указателя на этой статье: adug.org.au/technical/language/smart-pointers Я выбрал его, потому что это была самая простая реализация, которую я нашел. Но я все еще слишком новичок, чтобы судить о качестве этой реализации, поэтому я вынужден доверять тому, что я читал :-) Возможно, позже и с лучшими знаниями я пересмотрю свой класс интеллектуального указателя, но пока он работает так, как ожидалось для меня   -  person Jean-Milost Reymond    schedule 16.02.2017


Ответы (1)


Это довольно сложная ловушка, в которую вы попали. Когда вы пишете:

TGpGraphicsPath.Create

вы можете подумать, что вызываете конструктор без параметров. Но это не так. Фактически вы вызываете этот конструктор:

constructor Create(fillMode: TFillMode = FillModeAlternate); reintroduce; overload;      

Вы не указываете аргумент, поэтому значение по умолчанию предоставляется компилятором.

В вашем классе умного указателя вы пишете:

T.Create

Это действительно вызов конструктора без параметров. Но это конструктор, определенный TObject. Когда этот конструктор используется, экземпляр TGPGraphicsPath не инициализируется должным образом.

Если вы собираетесь использовать общее ограничение constructor, вы также должны убедиться, что вы всегда используете класс, который может быть правильно сконструирован с помощью конструктора без параметров. К сожалению для вас TGPGraphicsPath не отвечает всем требованиям. Действительно, таких классов преобладает.

Здесь действительно мало что можно сделать, чтобы избежать явного вызова конструктора. Вашему классу интеллектуального указателя практически невозможно определить, какой конструктор вызывать для этого конкретного класса.

Я бы посоветовал воздержаться от общего ограничения constructor и заставить потребителя класса интеллектуального указателя явно создать экземпляр экземпляра.

Это довольно распространенная проблема - я ответил на аналогичный вопрос здесь менее недели назад: Почему десериализованный словарь TDictionary работает некорректно?

person David Heffernan    schedule 15.02.2017
comment
Очень неприятная ошибка, сделанная исходным разработчиком дженериков / ограничений и копированием их из C #, где ctors автоматически наследуются. Использование ограничения ctor имеет смысл и фактически предотвращает использование классов, для которых не объявлен ctor без параметров, хотя их предок может иметь такой. В Delphi компилятор видит объект TObject, как только у дочернего класса есть объекты с добавленной перегрузкой. Но теперь, когда я написал это, я думаю, что это может быть исправлено компилятором, выполнив здесь правильное разрешение перегрузки, потому что в этих случаях он не должен видеть объект TObject. - person Stefan Glienke; 15.02.2017
comment
@stefan Я не думаю, что компилятор может когда-либо рассматривать конструктор с одним параметром с аргументом по умолчанию как конструктор без параметров, если только основной дизайн не будет радикально изменен. - person David Heffernan; 15.02.2017
comment
Я не понимаю, почему компилятор не может просто смотреть на доступные конструкторы в типе, и если конструктор без параметров найден, ищите конструктор, который имеет все параметры по умолчанию. Код пользователя в любом случае один и тот же, и обычно это нормально работает вне Generics, поэтому компилятор должен поступать правильно, независимо от того, находится ли код в Generic или нет. Это изменение не должно нарушать ограничение constructor, если его определение немного ослаблено, чтобы включить любой конструктор, который не требует входных параметров в коде пользователя. - person Remy Lebeau; 16.02.2017
comment
@remy Я не думаю, что текущий код дженериков может с этим справиться. Я считаю, что при вызове методов он должен знать аргументы на начальной стадии компиляции. И то, что вы предлагаете, требует, чтобы он ждал до каждого экземпляра. - person David Heffernan; 16.02.2017