О проблеме освобождения объектов |
16.11.2020 Виктор Кулик |
Предлагается способ так организовать создание и освобождение объектов, что при освобождении любой ссылки на объект обнилляются все ссылки на него.
При работе с объектами, да и вообще с указателями, очень часто на один и тот же объект ссылаются несколько переменных. Например, на форме, кроме переменной в описании класса формы, такая же ссылка присутствует в списке Components, а может быть и Controls.
При этом освобождение одной ссылки и присвоение ей nil, ровно ничего не делает с другими ссылками. Хуже того — и с самим объектом ровно ничего не происходит — просто память теперь считается свободной и может быть снова распределена. Но пока на это место ничего не записано, остальные ссылки ведут себя «как живые». В результате может пройти много времени, пока обращение к освобожденному объекту приведет к ошибке. Причем ошибки эти, как правило, относятся к невоспроизводимым. То есть возникают то в одной, то в другой ситуации, а могут и вообще не возникнуть до завершения программы. На мой взгляд такие ошибки являются одними из самых сложных для обнаружения.
На системном уровне никаких средств проверки действительности ссылок не предусмотрено. Думаю, в силу резкого снижения скорости выполнения программы. Ведь когда проверять? Надо проверять при каждом обращении к любому полю или методу. Это очень накладно. По этой же причине меня никогда не интересовали способы решения с помощью RTTI.
Года два назад мне пришел в голову способ как можно добиться, чтобы освобождение любой ссылки на объект автоматически обнуляло и все остальные. Я пытался найти в интернете такие же решения, но ничего не видел (по крайней мере в рунете, на зарубежных искал мало). Поэтому хочу предложить его на суд читателя.
Основной принцип состоит в том, чтобы использовать не прямые ссылки на объекты, а косвенные — то есть «ссылки на ссылки».
Стиль работы с такими переменными возвращает нас к временам от Турбо Паскаля и до появления Delphi, когда все ссылки на объекты стали по умолчанию употребляться без «шапочек». Итак,
Итого, в простейшем случае это может выглядеть так.
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, который рассматривается ниже.
Полный проект расположен на 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 вы получите ошибку, хотя его поля это дублированные ссылки, которые никто явно не освобождал.
Некоторые пояснения по тексту программы.
Программа состоит из трех модулей:
Основных комментариев заслуживает менеджер распределения памяти. Дело в том, что использовать отдельную переменную для того, чтобы создать объект, как это сделано в разделе «Суть метода», конечно, неестественно. Поэтому и был создан специальный класс 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));
Здесь для хранения первичных ссылок использован массив указателей, но, конечно, можно использовать и другие структуры (например, список). Однако надо помнить два правила:
Уверен, что предлагаемый способ может быть очень полезен в случае сложных программных комплексов. Откровенно говоря, очень удивительно, что я не нашел аналогов. Если кто-то знает, прошу не стесняться, но в виде ссылок, а не в виде: подумаешь, и я так делал, да не писал.
Если кто-то видит серьезные подводные камни, что ж давайте обсудим.
На всякий случай тестировал на WinXP i386, Win10 x64, Linux x64. Хотя ввиду тривиальности идеи должно работать на любой платформе, в том числе на Delphi.
В одном из своих проектов я эту технику использовал, но он не слишком большой, поэтому масштабного тестирования не было.
P.S. Указанный метод никоим образом не относится к потомкам TForm или TComponent. Те и так с такими проблемами неплохо справляются.