Утечка памяти Delphi или нарушение прав доступа при использовании объектов и интерфейсов

В настоящее время я немного застрял в отношении объектов и интерфейсов и их управления памятью. У меня есть класс, который наследуется от TInterfacedPersistent, называемый TAbstractBaseDTO. У меня также есть интерфейс IDuplicatable с функцией function CreateDuplicate: TAbstractBaseDTO.

Я использую интерфейсы для достижения абстракции, а не для управления памятью, и именно поэтому я использую TInterfacedPersistent как класс-предок.

ед. пас

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, System.Actions, Vcl.ActnList,
  Vcl.StdCtrls;

type
  TAbstractBaseDTO = class abstract (TInterfacedPersistent)
    public
      constructor CreateEmpty; virtual; abstract;
  end;

  IDuplicatable = interface
    ['{153275DC-71C0-4CB6-B933-667419950C68}']
    function CreateDuplicate: TAbstractBaseDTO;
  end;

  TCountryMasterDataDTO = class (TAbstractBaseDTO, IDuplicatable)
    public
      constructor CreateEmpty; override;
      function CreateDuplicate: TAbstractBaseDTO;
  end;

  TBaseDataForm = class(TForm)
    ActionList1: TActionList;
    actDuplicate: TAction;
    Button1: TButton;
    procedure actDuplicateExecute(Sender: TObject);
    strict private
      var
        FDataObject: TAbstractBaseDTO;
      function FetchBusinessObject: TAbstractBaseDTO;
    public
      procedure LoadData(ADataObject: TAbstractBaseDTO);
      destructor Destroy; override;
  end;

var
  BaseDataForm: TBaseDataForm;

implementation

{$R *.dfm}

procedure TBaseDataForm.LoadData(ADataObject: TAbstractBaseDTO);
begin
  FDataObject.Free;
  FDataObject := ADataObject;
end;

destructor TBaseDataForm.Destroy;
begin
  FDataObject.Free;
  inherited;
end;

constructor TCountryMasterDataDTO.CreateEmpty;
begin
  Create;
end;

function TCountryMasterDataDTO.CreateDuplicate: TAbstractBaseDTO;
begin
  Result := TCountryMasterDataDTO.Create;
end;

procedure TBaseDataForm.actDuplicateExecute(Sender: TObject);
var
  LAbstractBaseDTO: IDuplicatable;
  LAbstractBaseDTODuplicate: TAbstractBaseDTO;
begin
  LAbstractBaseDTO := Self.FetchBusinessObject as IDuplicatable;
  try
    LAbstractBaseDTODuplicate := LAbstractBaseDTO.CreateDuplicate;
    Self.LoadData(LAbstractBaseDTODuplicate);
  finally
    LAbstractBaseDTO := nil;
  end;
end;

function TBaseDataForm.FetchBusinessObject: TAbstractBaseDTO;
begin
  Result := TCountryMasterDataDTO.Create;
end;

end.

Когда я запускаю действие actDuplicate, FastMM4 сообщает об утечке памяти при завершении работы.

Однако изменение функции выполнения на:

procedure TBaseDataForm.actDuplicateExecute(Sender: TObject);
var
  LAbstractBaseDTO: TAbstractBaseDTO;
  LAbstractBaseDTODuplicate: TAbstractBaseDTO;
begin
  LAbstractBaseDTO := Self.FetchBusinessObject;
  try
    LAbstractBaseDTODuplicate := (LAbstractBaseDTO as IDuplicatable).CreateDuplicate;
    Self.LoadData(LAbstractBaseDTODuplicate);
  finally
    LAbstractBaseDTO.Free;
  end;
end;

немедленно вызовет нарушение прав доступа при запуске действия.

Как я могу решить эту проблему?

Создает ли FetchBusinessObject новый объект, который нужно освободить? Почему CreateDuplicate возвращает ссылку на объект вместо интерфейса? Как LAbstractBaseDTODuplicate используется после создания? Пожалуйста, предоставьте минимальный воспроизводимый пример

Remy Lebeau 10.04.2023 17:19

Да, результат от FetchBusinessObject нужно где-то освобождать. На самом деле LAbstractBaseDTODuplicate будет загружен в форму для заполнения FDMemTable. Также эта форма освободит текущий объект и заменит его на LAbstractBaseDTODuplicate. Я постараюсь добавить только соответствующий код, так как все это происходит в нескольких формах. Я также изменил метод интерфейса, чтобы он возвращал IDuplicatable вместо TAbstractBaseDTO.

Raphael 10.04.2023 18:00

Единственная веская причина когда-либо получать от TInterfacedPersistent — включить interface подсчет ссылок. Это означает, что любой созданный вами объект, производный от TInterfacedPersistent, должен использоваться только через interface ссылки, а не object ссылки. Если вам нужны ссылки на объекты, не используйте TInterfacedPersistent, вместо этого используйте TPersistent и вручную Free объекты, когда это необходимо.

Remy Lebeau 10.04.2023 19:42

Если на то пошло, мне интересно, почему вы вообще используете T(Interfaced)Persistent? Эти базовые классы предназначены для включения RTTI для классов, например для потоковой передачи DFM, которые не используются в показанном коде (т. е. без свойств published).

Remy Lebeau 10.04.2023 19:44

Хорошо спасибо! На самом деле я видел использование интерфейса, больше похожего на контейнер с определенным поведением («может быть продублировано»), чем на самом деле позволить delphi управлять памятью для меня. Я понял, что если я перемещу весь контент из actDuplicateExecute в конкретную форму и просто вызову его из базовой формы, это сработает. Не совсем уверен, где что-то не так с бросками. Создав минимальный воспроизводимый пример, ошибки к сожалению тоже не возникло. Так что, наверное, я что-то напутал с гипсом. В любом случае, теперь это работает, и спасибо Дэвиду и Реми.

Raphael 10.04.2023 20:57

Основная проблема здесь заключается в том, что вы смешиваете ссылки на интерфейсы со ссылками на объекты. Ваш код немного запутан, поэтому трудно сказать, что именно там не так. Вы должны создать правильный минимальный воспроизводимый пример, если хотите получить более конкретные ответы. TInterfacedPersistent класс имеет отключенный подсчет ссылок, и вам нужно вручную управлять его памятью. Однако в тот момент, когда вы освобождаете такой объект, вы должны убедиться, что нет интерфейсов, указывающих на него, потому что, если они есть, у вас будет AV, когда эта ссылка выйдет за пределы области видимости, и метод интерфейса _Release будет вызываться в мертвом экземпляре.

Dalija Prasnikar 10.04.2023 21:27

Если вы хотите задействовать интерфейсы, то было бы проще оставить управление памятью интерфейсам и использовать TInterfacedObject в качестве базового класса, поскольку проектирование иерархий классов вокруг классов с отключенным подсчетом ссылок требует немного больше знаний (из-за более тонких проблем, которые могут возникнуть с приведением и хранение ссылок), что с использованием подсчета ссылок.

Dalija Prasnikar 10.04.2023 21:31

Но, глядя на ваши классы и тот факт, что у вас есть базовый класс, зачем вообще использовать интерфейс? Просто добавьте виртуальную функцию CreateDuplicate в базовый класс и все. Если некоторые из классов-потомков не могут быть продублированы, вы можете либо вернуть nil из CreateDuplicate как указание на то, что объект не может быть продублирован, либо добавить другую виртуальную функцию IsDuplicatable: Boolean, которая сообщит вам, поддерживает ли конкретный класс-потомок дублирование или нет.

Dalija Prasnikar 10.04.2023 21:36

Спасибо, Далия! Я пытался добиться именно этого поведения (дублируемого или нет) с помощью интерфейса, чтобы реализовать только функцию, в которой реализован интерфейс, и позволить функции Support обрабатывать все проверки для меня :). Как я уже говорил, я могу сам управлять памятью (освобождая объекты). Это не причина, по которой я хотел использовать интерфейс здесь. Это больше похоже на описание того, что могут делать мои классы.

Raphael 10.04.2023 23:03

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

Dalija Prasnikar 10.04.2023 23:22

Опять же, если вы хотите получить ответы на свой вопрос, вам нужно немного сократить приведенный выше код и удалить все лишнее. Вам не нужно полное объявление объекта со всеми полями. Также вы разместили два объявления интерфейса IDuplicatable, одно из которых возвращает тип IDuplicatable, а другое возвращает тип TAbstractBaseDTO. Какой из них находится в вашем реальном коде? Это может радикально изменить то, что происходит в вашем коде.

Dalija Prasnikar 10.04.2023 23:31

Кроме того, у вас слишком много ненужного приведения типов. из вашего кода кажется, что вы хотите вернуть TAbstractBaseDTO из CreateDuplicate, а не IDuplicatable, поскольку, как только у вас есть дубликат, вы хотите использовать этот экземпляр для других целей, а не вызывать CreateDuplicate снова, поэтому нет смысла возвращать IDuplicatable. Далее, зачем вообще нужно создавать дубликаты? Поскольку вы освобождаете LAbstractBaseDTO, который возвращается FetchBusinessObject, почему бы вам не передать этот объект LoadData напрямую?

Dalija Prasnikar 10.04.2023 23:37

Еще раз спасибо @DalijaPrasnikar. Изначально я просто хочу вернуть TAbstractBaseDTO от CreateDuplicate. Извините, что не указал, метод CreateDuplicate не возвращает точную копию объекта, а копирует одни значения и сбрасывает другие. Но, возможно, важно отметить, что это новый объект, который нужно освободить. Поэтому я хочу, чтобы эта логика в классе была своего рода бизнес-логикой, и загружала этот новый объект в метод LoadData, а не только старый. Вы правы, мне не нужен результат как IDuplicatable, поэтому я изменил его обратно на возвращаемый тип TAbstractBaseDTO.

Raphael 11.04.2023 12:17
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
13
147
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Проблема, с которой вы столкнулись, заключается в том, что существует живая ссылка интерфейса на объект, который вы уже уничтожили.

В данном случае это скрытая неявная ссылка на интерфейс, созданная компилятором при преобразовании типа LAbstractBaseDTO как IDuplicatable.

LAbstractBaseDTODuplicate := (LAbstractBaseDTO as IDuplicatable).CreateDuplicate;

Эта скрытая ссылка на интерфейс будет очищена компилятором в эпилоге метода, и это вызовет метод _Release для уже уничтоженного экземпляра объекта.

Если вы запустите свой код с FASTMM в режиме полной отладки, FASTMM обнаружит такой сценарий и покажет сообщение об ошибке, за которым следует трассировка стека:

FASTMM обнаружил попытку использования интерфейса освобожденного объекта. Теперь будет выдано нарушение прав доступа, чтобы прервать текущую операцию.

Чтобы решить эту проблему, вам нужно преобразовать эту неявную ссылку на интерфейс в явную под вашим контролем, а затем вы можете очистить эту ссылку самостоятельно, прежде чем освободить объект, на который она ссылается.

procedure TBaseDataForm.actDuplicateExecute(Sender: TObject);
var
  LAbstractBaseDTO: TAbstractBaseDTO;
  LAbstractBaseDTODuplicate: TAbstractBaseDTO;
  LDuplicatable: IDuplicatable;
begin
  LAbstractBaseDTO := FetchBusinessObject;
  try
    // save interface reference to a variable
    LDuplicatable := LAbstractBaseDTO as IDuplicatable;
    LAbstractBaseDTODuplicate := LDuplicatable.CreateDuplicate;
    LoadData(LAbstractBaseDTODuplicate);
  finally
    LDuplicatable := nil;
    LAbstractBaseDTO.Free;
  end;
end;

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