Почему освобождение компонента-члена в деструкторе класса вызывает ошибку EInvalidPointer при закрытии приложения?

#delphi #firemonkey #delphi-10.4-sydney

#delphi #firemonkey #delphi-10.4-сидней

Вопрос:

Вот класс, который я создал для добавления метки TLabel в TTrackBar. Метка показывает значение trackbar при перетаскивании, а затем исчезает. Экземпляр создается во время выполнения, а родительский элемент устанавливается в форму. Он работает нормально, но выдает ошибку при закрытии приложения, если панель отслеживания все еще существует. Однако нет никаких проблем, если панель отслеживания освобождается во время выполнения, а затем приложение закрывается. При отладке этой строки при закрытии приложения (FLabel.Бесплатно;) Я вижу, что FLabel и данные в нем все еще существуют, но он по-прежнему выдает эту ошибку. Я обеспокоен тем, что если я просто удалю эту строку, то при освобождении объекта во время выполнения произойдет утечка памяти. Я попытался изменить его на if Assigned(FLabel), а затем на FLabel.Бесплатно; но без изменений. Я знаю, что это должно быть как-то связано с тем фактом, что родительский элемент метки был установлен.

 unit TrackBarLabelUnit;

interface

uses
  System.Types, System.Classes, System.SysUtils, FMX.Types, FMX.StdCtrls,
  FMX.Controls;

type
  TValueToString = function(AValue : Single) : String of object;

  TTrackBarLabel = class(TTrackBar)
  private
    FLabel : TLabel;
    FSuffix : String;
    FTimer : TTimer;
    FOffset : Integer;
    FValueToString : TValueToString;

    procedure TimerTimer(Sender: TObject);
  protected
    procedure ParentChanged; override;
    procedure DoTracking; override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;

    property Suffix : String read FSuffix write FSuffix;
    property LabelOffset : Integer read FOffset write FOffset;
    property ValueToString : TValueToString write FValueToString;
  end;

implementation

constructor TTrackBarLabel.Create(AOwner: TComponent);
begin
  inherited;
  FLabel := TLabel.Create(nil);
  FLabel.Visible := False;
  FTimer := TTimer.Create(nil);
  FTimer.Interval := 100;
  FTimer.Enabled := False;
  FTimer.OnTimer := TimerTimer;
  FSuffix := '';
  FOffset := 22;
end;

destructor TTrackBarLabel.Destroy;
begin
  FLabel.Free; // EInvalidPointer error here when application is closed
  FTimer.Free;
  inherited Destroy;
end;

procedure TTrackBarLabel.ParentChanged;
begin
  inherited;
  FLabel.Parent := Parent;
end;

procedure TTrackBarLabel.DoTracking;
begin
  inherited;

  if not Assigned(Thumb) then Exit;

  FLabel.Visible := True;
  FLabel.Tag := 10;
  FLabel.Opacity := 1;

  if Assigned(FValueToString) then
    FLabel.Text := FValueToString(Value)   FSuffix
  else
    FLabel.Text := FloatToStrF(Value, ffFixed, 12, 1)   FSuffix;

  if Orientation = TOrientation.Horizontal then begin
    FLabel.Position.X := Position.X   Thumb.Position.X  
                         (Thumb.Width - FLabel.Width) * 0.5;
    FLabel.Position.Y := Position.Y   FOffset;
    FLabel.TextSettings.HorzAlign := TTextAlign.Center;
  end else begin
    FLabel.Position.X := Position.X   FOffset;
    FLabel.Position.Y := Position.Y   Thumb.Position.Y - 2;
    FLabel.TextSettings.HorzAlign := TTextAlign.Leading;
  end;

  FTimer.Enabled := False;
  FTimer.Enabled := True;
end;

procedure TTrackBarLabel.TimerTimer(Sender: TObject);
begin
  FLabel.Tag := FLabel.Tag - 1;
  FLabel.Opacity := FLabel.Tag * 0.2;
  if FLabel.Tag < 0 then begin
    FLabel.Visible := False;
    FTimer.Enabled := False;
  end;
end;

end.
 

Ответ №1:

Чаще всего исключение с недопустимым указателем означает, что вы пытаетесь освободить объект дважды.

Проблема в этом случае заключается в том, что элемент управления освобождает своих дочерних элементов при его освобождении. Поэтому, когда форма освобождается, она также освобождает TLabel . Таким образом, когда TTrackBarLabel.Destroy выполняется your, your FLabel является висячим указателем, и вы не должны этого делать FLabel.Free .

Все разработчики Delphi знают, что компонент освобождает принадлежащие ему компоненты при его освобождении. Менее известный факт, что элемент управления также освобождает своих дочерних элементов.

В вашем случае вы можете просто удалить FLabel.Free . Однако это приведет к утечке памяти, если вы никогда не устанавливали свойство FLabel ‘s Parent .

Чтобы убедиться, что метка автоматически освобождается, когда панель треков, сделайте панель треков владельцем метки:

   FLabel := TLabel.Create(Self);
 

В качестве отступления, ваше предложение

 if Assigned(FLabel) then
  FLabel.Free;
 

не поможет, потому FLabel что при возникновении ошибки появляется висячий указатель (это не nil так).

Кроме того, в Delphi вы никогда не пишете

 if Assigned(FLabel) then
  FLabel.Free;
 

потому TObject.Free что в основном это if Assigned then Destroy так

 if Assigned(FLabel) then
  FLabel.Free;
 

означает

 if Assigned(FLabel) then
  if Assigned(FLabel) then
    FLabel.Destroy;
 

что очень глупо.

Комментарии:

1. Спасибо. Итак, в итоге мне не нужно освобождать его самостоятельно, и чтобы убедиться в этом, я делаю панель треков владельцем метки. Я сделал это изначально, но хотел освободить его сам, чтобы убедиться, что он был освобожден. Я где-то читал, что создание метки с nil означало бы, что я буду нести ответственность за ее освобождение сам, но, похоже, это не так, поскольку форма все равно освобождает своих дочерних элементов. Почему нет проблемы с TTimer? Я знаю, что родительский элемент не задан как метка, но если я создаю его с помощью трекбара в качестве владельца, у меня все равно не возникает никаких проблем при его освобождении.

2. @XylemFlow: (1) Правильно. Удалите свой FLabel.Free и напишите TLabel.Create(Self) , чтобы сделать панель треков владельцем метки. (2) Действительно, «все» знают, что компонент освобождает принадлежащие Owner ему компоненты () при его освобождении, но не так хорошо известно, что элемент управления освобождает свои дочерние компоненты ( Parent ) при его освобождении. (3) TTimer Это компонент ( TComponent ), но не элемент управления ( TControl ). Это не то, что отображается на экране; у него нет родительского элемента. Если вы создадите его с панелью отслеживания в качестве ее владельца, оно действительно будет освобождено, когда панель отслеживания будет, но когда вы это сделаете FTimer.Free

3. … владелец (панель отслеживания) уведомляется об этом, и таймер удаляется из списка принадлежащих владельцу компонентов.

4. Спасибо. Похоже, тогда мне вообще не нужен деструктор. Я предполагаю, что деструктор действительно нужен только для освобождения объектов, у которых нет владельца, или для очистки массива.

5. Технически вам нужен деструктор, но вам не нужно вызывать его самостоятельно, потому что библиотека времени выполнения сделает это за вас (в данном случае, когда владелец освобожден).