Подсчет ссылок для интерфейсов
Ваш первоначальный вопрос и ответ в комментариях к этому ответу полностью зависят от механизма подсчета ссылок в интерфейсе Delphi.
Компилятор выдает код, чтобы организовать подсчет всех ссылок на интерфейс. Всякий раз, когда вы берете новую ссылку, количество увеличивается. Всякий раз, когда ссылка освобождается (устанавливается на nil
, выходит за пределы области и т. Д.), Счет уменьшается. Когда счетчик достигает нуля, интерфейс освобождается, и в вашем случае это то, что вызывает Free
для ваших объектов.
Ваша проблема в том, что вы обманываете подсчет ссылок, помещая интерфейсные ссылки в TList
и выводя из него Pointer
и обратно. Где-то по пути ссылки неверно учтены. Я уверен, что поведение вашего кода (то есть переполнение стека) можно объяснить, но я не склонен пытаться это делать, поскольку в коде используются такие явно некорректные конструкции.
Проще говоря, вы никогда не должны приводить интерфейс к неуправляемому типу, например Pointer
. Всякий раз, когда вы делаете это, вы также должны взять под контроль отсутствующий код подсчета ссылок. Уверяю вас, это то, что вы не хотите брать на себя!
Вы должны использовать правильный типобезопасный контейнер, такой как TList<INode>
или даже динамический массив, и тогда подсчет ссылок будет обработан правильно. Внесение этого изменения в ваш код решает проблемы, которые вы описываете в вопросе.
Циркулярные ссылки
Однако остается одна большая проблема, которую вы обнаружили для себя и подробно изложили в комментариях.
Как только вы будете следовать правилам подсчета ссылок, вы столкнетесь с проблемой циклических ссылок. В этом случае узел содержит ссылку на контейнер, который в свою очередь содержит ссылку на узел. Подобные циклические ссылки не могут быть нарушены стандартным механизмом подсчета ссылок, и вы должны разбить их самостоятельно. После того как вы разорвете одну из двух отдельных ссылок, составляющих циклическую ссылку, фреймворк сделает все остальное.
С вашим текущим дизайном вы должны прервать циклические ссылки, явно вызывая UnReg
на каждом INode
, который вы создаете.
Другая проблема с кодом в его нынешнем виде заключается в том, что вы используете поля данных формы для хранения MyContainer
, MyNode
и т. Д. Поскольку вы никогда не устанавливаете MyContainer
в nil
, тогда два выполнения вашего события обработчик приведет к утечке.
Внесены следующие изменения в ваш код, чтобы доказать, что он будет работать без утечек:
TContainer = class(TInterfacedObject, IContainer)
protected
NodeList: TList<INode>;//switch to type-safe list
...
procedure TContainer.RegisterNode(Node:INode);
begin
//must ensure we don't add the node twice
if NodeList.IndexOf(Node) = -1 then
NodeList.Add(Node);
end;
...
procedure TForm1.btnMakeStuffClick(Sender: TObject);
//make the interfaces local variables although in production
//code they would likely be fields and construction would happen
//in the constructor of the owning object
var
MyContainer: IContainer;
MyNode1, MyNode2, MyNode3: INode;
begin
MyContainer := TContainer.Create;
MyNode1 := TNode.Create(MyContainer);
MyNode2 := TNode.Create(MyContainer);
MyNode3 := TNode.Create(MyContainer);
MyNode1.UnReg;
MyNode1.ReReg(MyContainer);
MyNode2.UnReg;
MyNode3.UnReg;
MyNode2.ReReg(MyContainer);
MyNode1.UnReg;
MyNode2.UnReg;
end;
С этими изменениями код работает без утечек памяти - установите ReportMemoryLeaksOnShutdown := True
в начале файла .dpr для проверки.
Это будет что-то вроде привязки, чтобы вызывать UnReg
на каждом узле, поэтому я предлагаю вам просто добавить метод к IContainer
, чтобы сделать это. Как только вы решите, что контейнер может отбрасывать свои ссылки, у вас будет гораздо более управляемая система.
Вы не сможете позволить подсчету ссылок делать всю работу за вас. Вам нужно будет явно позвонить IContainer.UnRegAllItems
.
Вы можете реализовать этот новый метод следующим образом:
procedure TContainer.UnRegAllItems;
begin
while NodeList.Count>0 do
NodeList[0].UnReg;
end;
Ошибки подсчета ссылок
Хотя механизм подсчета ссылок Delphi в целом очень хорошо реализован, насколько мне известно, существует одна давняя и очень известная ошибка.
procedure Foo(const I: IInterface);
begin
I.DoSomething;
end;
...
Foo(TInterfacedObject.Create);
Когда вызывается таким образом Foo
, код для добавления ссылки на интерфейс не генерируется. Таким образом, интерфейс освобождается, как только он создается, и Foo
действует на недопустимый интерфейс.
Поскольку Foo
получает параметр как const
, Foo
не принимает ссылку на интерфейс. Ошибка в codegen для вызова Foo
, который по ошибке не принимает ссылку на интерфейс.
Мой предпочтительный способ обойти эту проблему так:
var
I: IInterface;
...
I := TInterfacedObject.Create;
Foo(I);
Это успешно, потому что мы явно берем ссылку.
Обратите внимание, что я объяснил это для дальнейшего использования - ваш текущий код не противоречит этой проблеме.