Публикации FreePascal

О проблеме освобождения объектов

16.11.2020
Виктор Кулик
  1. Аннотация
  2. Введение
  3. Суть метода
  4. Демонстрационный пример SmartFree (UG)
  5. Демонстрационный пример SmartFree (PG)
  6. Заключение

1. Аннотация

Предлагается способ так организовать создание и освобождение объектов, что при освобождении любой ссылки на объект обнилляются все ссылки на него.

2. Введение

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

При этом освобождение одной ссылки и присвоение ей nil, ровно ничего не делает с другими ссылками. Хуже того — и с самим объектом ровно ничего не происходит — просто память теперь считается свободной и может быть снова распределена. Но пока на это место ничего не записано, остальные ссылки ведут себя «как живые». В результате может пройти много времени, пока обращение к освобожденному объекту приведет к ошибке. Причем ошибки эти, как правило, относятся к невоспроизводимым. То есть возникают то в одной, то в другой ситуации, а могут и вообще не возникнуть до завершения программы. На мой взгляд такие ошибки являются одними из самых сложных для обнаружения.

На системном уровне никаких средств проверки действительности ссылок не предусмотрено. Думаю, в силу резкого снижения скорости выполнения программы. Ведь когда проверять? Надо проверять при каждом обращении к любому полю или методу. Это очень накладно. По этой же причине меня никогда не интересовали способы решения с помощью RTTI.

Года два назад мне пришел в голову способ как можно добиться, чтобы освобождение любой ссылки на объект автоматически обнуляло и все остальные. Я пытался найти в интернете такие же решения, но ничего не видел (по крайней мере в рунете, на зарубежных искал мало). Поэтому хочу предложить его на суд читателя.

3. Суть метода

Основной принцип состоит в том, чтобы использовать не прямые ссылки на объекты, а косвенные — то есть «ссылки на ссылки».

Стиль работы с такими переменными возвращает нас к временам от Турбо Паскаля и до появления Delphi, когда все ссылки на объекты стали по умолчанию употребляться без «шапочек». Итак,

  • Для каждого класса определяется и тип «ссылка на класс»
    PSomeClass = ^TSomeClass;
    TSomeClass = class(...
  • Переменные — объекты класса объявляются с типом ссылки на класс. Чтобы не путать их с обычными я использую для них префикс «x»
    xSC: PSomeClass
  • При использовании переменной следует использовать «шапочку» — xSC^.SomeMethod

Итого, в простейшем случае это может выглядеть так.

program
....
 
type
  PSomeClass = ^TSomeClass;
  TSomeClass = class(TObject)
    Value: integer;
    procedure SomeMethod;
    constructor Create(AValue: integer);
  end;
 
var
  SC: TSomeClass; // нужна только для размещения. После размещения ей не пользуемся
  xSC1: PSomeClass;
  xSC2: PSomeClass;
.....
begin
  SC := TSomeClass.Create(5);
  xSC1 := @SC;
  xSC2 := xSC1; // две косвенные ссылки на один объект
  xSC1^.SomeMethod;
........
  xSC2^.SomeMethod;
  FreeAndNil(xSC2^); // обязательно с шапочкой
........
  xSC1^.SomeMethod; // будет ошибка обращения по адресу nil

Для демонстрации был написан примерчик SmartFree, который рассматривается ниже.

4. Демонстрационный пример SmartFree (UG)

Полный проект расположен на github.com/Kulic59/SmartFree.git.

В демонстрационном примере создаются три объекта xIntObj, xStrObj и xObjX. Первые два содержат, соответственно, целое и строковое поле, а xObjX – два поля первых двух типов. Все эти классы умеют вывести значения своих полей в TStrings .

Главное окно программы имеет вид

Кнопкой Create создаются все три объекта. При этом xIntObj получит значение из поля Int (в данном случае 15), а xStrObj из поля Str (test), а xObjX — значения первых двух.

Кнопкой Print можно вывести на Memo содержание всех трех объектов, а кнопкой PrintObjX только xObjX, который содержит «дополнительные» ссылки на xIntObj и xStrObj.

Остальные имеют достаточно говорящие названия.

При тестировании надо сначала создать кнопкой Create объекты, потом проверить их содержимое кнопкой Print. Потом освободить FreeIntObj и/или FreeStrObj. Теперь при нажатии на PrintObjX вы получите ошибку, хотя его поля это дублированные ссылки, которые никто явно не освобождал.

5. Демонстрационный пример SmartFree (PG)

Некоторые пояснения по тексту программы.

Программа состоит из трех модулей:

  • UfmSmartFree — главное окно программы
  • ObjSamples — определение основных классов TIntObj, TStrObj, TObjX.
  • xVMemDriver – менеджер распределения памяти для Х-объектов

Основных комментариев заслуживает менеджер распределения памяти. Дело в том, что использовать отдельную переменную для того, чтобы создать объект, как это сделано в разделе «Суть метода», конечно, неестественно. Поэтому и был создан специальный класс TPtrList, который служит для хранения первичных ссылок на объекты.

unit xVMemDriver;

{$mode Delphi}

interface
uses
  Classes, contnrs;

type

  PObject = ^TObject;

  { TPtrList }

  TPtrList=class
  private
    MaxCount: integer; // Полная емкость списка
    Count: integer; // присвоенная емкость
    PtrList: array of pointer;
  public
    constructor Create(AMaxCount: integer);
    function AddXObj(Obj: TObject): pointer;
    destructor Destroy; override;
  end;

var
  MemList: TPtrList;

implementation
uses
  LCL, SysUtils;

{ TPtrList }

function TPtrList.AddXObj(Obj: TObject): pointer;
begin
  if Obj=nil then exit;
  {if Count=MaxCount then
  begin
    CheckListForNil;
    if NilCount=0 then
      raise Exception.Create('X Memory pool overflow');
  end;}
  inc(Count);
  PtrList[Count-1] := Obj;
  result := @PtrList[Count-1];
end;

constructor TPtrList.Create(AMaxCount: integer);
begin
  SetLength(PtrList,AMaxCount);
  MaxCount := AMaxCount;
  Count := 0;
end;

destructor TPtrList.Destroy;
var
  i: integer;
  Item: TObject;
begin
  for i:=0 to count-1 do
  begin
    Item := TObject(PtrList[i]);
    if (Item<>nil) and (Item is TObject) then
      FreeAndNil(Item);
  end;
  inherited;
end;

initialization
  MemList := TPtrList.Create(1000);
finalization
  FreeAndNil(MemList);

end.

Ключевым методом служит AddXObj, который в качестве параметра принимает создаваемый объект, а возвращает ссылку на адрес, где хранится Delphi-переменная. Теперь для создания Х-объекта можно использовать

xIntObj := MemList.AddXObj(TIntObj.Create(15));

Здесь для хранения первичных ссылок использован массив указателей, но, конечно, можно использовать и другие структуры (например, список). Однако надо помнить два правила:

  1. Нельзя, чтобы изменялись адреса ячеек, где хранятся первичные указатели. Например, в TPtrList нельзя увеличивать размер массива с помощью SetLength, потому что в результате весь массив может быть переписан на новое место.
  2. Новые элементы должны только добавляться. Нельзя производить ревизию и пытаться использовать повторно те ячейки, в которых оказались nil (объекты были освобождены). В противном случае вся идея будет дискредитирована. Отсюда следует, что указанный метод малопригоден, если планируется работать с большим количеством маленьких (по памяти) объектов. Типа моих TIntObj.

6. Заключение

Уверен, что предлагаемый способ может быть очень полезен в случае сложных программных комплексов. Откровенно говоря, очень удивительно, что я не нашел аналогов. Если кто-то знает, прошу не стесняться, но в виде ссылок, а не в виде: подумаешь, и я так делал, да не писал.

Если кто-то видит серьезные подводные камни, что ж давайте обсудим.

На всякий случай тестировал на WinXP i386, Win10 x64, Linux x64. Хотя ввиду тривиальности идеи должно работать на любой платформе, в том числе на Delphi.

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

P.S. Указанный метод никоим образом не относится к потомкам TForm или TComponent. Те и так с такими проблемами неплохо справляются.

Актуальные версии
FPC3.2.0release
Lazarus2.0.10release
MSE5.0.0release
fpGUI1.4.1release
наши спонсоры ;)