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

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

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.
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
0
138
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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

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

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

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

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

  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;

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

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

XylemFlow 23.12.2020 10:21

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

Andreas Rejbrand 23.12.2020 11:45

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

Andreas Rejbrand 23.12.2020 11:46

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

XylemFlow 23.12.2020 15:08

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

Andreas Rejbrand 23.12.2020 15:09

Чтобы избежать проблемы с оборванным указателем, вызовите FLabel.FreeNotification(Self), а затем переопределите виртуальный Notification(), чтобы установить FLabel := nil при уничтожении метки. Если Self используется в качестве владельца, FreeNotification() вызывается автоматически для вас, но вам все равно нужно реализовать Notification()

Remy Lebeau 25.12.2020 19:19

@RemyLebeau: Верно, но в большинстве ситуаций более простого подхода, описанного в моем A, конечно, вполне достаточно. Но это хороший комментарий для тех, у кого есть особые потребности.

Andreas Rejbrand 25.12.2020 19:24

@AndreasRejbrand в некотором роде, это особый случай, поскольку лейбл имеет другой Parent, чем его Owner. Как вы сказали, Parent может освободить метку до того, как освободит TrackBar, поэтому лучше не оставлять висячий указатель на волю случая.

Remy Lebeau 25.12.2020 19:41

@RemyLebeau: Да, вы можете ссылаться на FLabel в других местах, кроме деструктора. Это может быть опасно, и если это так, вам действительно может понадобиться уведомление.

Andreas Rejbrand 25.12.2020 20:18

Другие вопросы по теме