В Delphi, почему передача переменной Interface иногда требует, чтобы она была параметром const? - PullRequest
16 голосов
/ 04 октября 2011

Сначала вопрос: почему удаление const в UnregisterNode() вызывает сбой, а не в RegisterNode().

Теперь предыстория: я работаю в Delphi XE с интерфейсами, и я столкнулся с артефактом, который дал мне некоторую паузу, и я пришел к выводу, что я действительно не понимаю, почему.

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

Нажатие одинокой кнопки в форме создает контейнер и три узла. Операции над ними выполняются для демонстрации процедуры

Программа создает несколько простых узлов, которые ссылаются на простой контейнер. Проблема возникла в случаях № 1 и № 6. Когда узел освобождается, он вызывает метод контейнеров Unregister(). Метод удаляет копию указателя на узел в TList. При выходе из метода в двух неудачных случаях он вызывает метод узла Destroy(), рекурсивно запускающий процесс заново, пока не произойдет переполнение стека.

В четырех случаях, которые работают, метод Destroy() возобновляется как обычно, и программа продолжит работу и выйдет в обычном режиме.

Ошибка № 1 (Случай 1)

procedure RegisterNode(Node:INode);
procedure UnregisterNode(Node:INode);

Вызов узла Unregister() из метода TNode.Destroy(), похоже, влияет на счетчик ссылок INode, вызывая множественные вызовы Destroy(). Почему это происходит, я не понимаю. Это не происходит, когда Я Register() узел с тем же стилем параметров.

Ошибка № 2 (Случай 6)

procedure RegisterNode(const Node:INode);
procedure UnregisterNode(Node:INode);

Та же самая схема сбоя происходит здесь. Добавление const в список параметров, как в случае 5, предотвращает рекурсивные вызовы Destroy().

код:

unit fMain;
{
   Case 1 - Fails when a node is freed, after unregistering,
             TNode.Destroy is called again
   Case 2 - Passes
   case 3 - Passes
   Case 4 - Passes
   Case 5 - Passes
   Case 6 - Fails the same way as case 1
}
{$Define Case1}
{.$Define Case2}
{.$Define Case3}
{.$Define Case4}
{.$Define Case5}
{.$Define Case6}
interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type

  INode = interface;
  TNode = class;

  IContainer = interface
  ['{E8B2290E-AF97-4ECC-9C4D-DEE7BA6A153C}']
{$ifDef Case1}
    procedure RegisterNode(Node:INode);
    procedure UnregisterNode(Node:INode);
{$endIf}
{$ifDef Case2}
    procedure RegisterNode(Node:TNode);
    procedure UnregisterNode(Node:TNode);
{$endIf}
{$ifDef Case3}
    procedure RegisterNode(const Node:INode);
    procedure UnregisterNode(const Node:INode);
{$endIf}
{$ifDef Case4}
    procedure RegisterNode(const Node:TNode);
    procedure UnregisterNode(const Node:TNode);
{$endIf}
{$ifDef Case5}
    procedure RegisterNode(Node:INode);
    procedure UnregisterNode(const Node:INode);
{$endIf}
{$ifDef Case6}
    procedure RegisterNode(const Node:INode);
    procedure UnregisterNode(Node:INode);
{$endIf}
  end;
  INode = interface
  ['{37923052-D6D1-4ED5-9AC0-F7FB0076FED8}']
    procedure SetContainer(const Value:IContainer);
    function GetContainer():IContainer;
    procedure ReReg(const AContainer: IContainer);
    procedure UnReg();
    property Container : IContainer
      read GetContainer write SetContainer;
  end;

  TContainer = class(TInterfacedObject, IContainer)
  protected
    NodeList: TList;
  public
    constructor Create(); virtual;
    destructor Destroy(); override;
{$ifDef Case1}
    procedure RegisterNode(Node:INode); virtual;
    procedure UnregisterNode(Node:INode); virtual;
{$endIf}
{$ifDef Case2}
    procedure RegisterNode(Node:TNode); virtual;
    procedure UnregisterNode(Node:TNode); virtual;
{$endIf}
{$ifDef Case3}
    procedure RegisterNode(const Node:INode); virtual;
    procedure UnregisterNode(const Node:INode); virtual;
{$endIf}
{$ifDef Case4}
    procedure RegisterNode(const Node:TNode); virtual;
    procedure UnregisterNode(const Node:TNode); virtual;
{$endIf}
{$ifDef Case5}
    procedure RegisterNode(Node:INode); virtual;
    procedure UnregisterNode(const Node:INode); virtual;
{$endIf}
{$ifDef Case6}
    procedure RegisterNode(const Node:INode); virtual;
    procedure UnregisterNode(Node:INode); virtual;
{$endIf}
  end;

  TNode = class(TInterfacedObject, INode)
  protected
    FContainer : IContainer;
  public
    constructor Create(const AContainer: IContainer); virtual;
    destructor Destroy(); override;
    procedure SetContainer(const Value:IContainer); virtual;
    function GetContainer():IContainer; virtual;
    procedure ReReg(const AContainer: IContainer); virtual;
    procedure UnReg(); virtual;
    property Container : IContainer
      read GetContainer write SetContainer;
  end;

  TForm1 = class(TForm)
    btnMakeStuff: TButton;
    procedure btnMakeStuffClick(Sender: TObject);
  private
    { Private declarations }
    MyContainer : IContainer;
    MyNode1,
    MyNode2,
    MyNode3     : INode;

  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation
{$R *.dfm}

{ TContainer }

constructor TContainer.Create();
begin
  inherited;
  NodeList := TList.Create();
end;
destructor TContainer.Destroy();
var
  i : integer;
begin
  for i := 0 to Pred(NodeList.Count) do
    INode(NodeList.Items[i]).Container := nil;  //Prevent future Node from contacting container
  NodeList.Free();
  inherited;
end;

{$ifDef Case1}
procedure TContainer.RegisterNode(Node:INode);
{$endIf}
{$ifDef Case2}
procedure TContainer.RegisterNode(Node:TNode);
{$endIf}
{$ifDef Case3}
procedure TContainer.RegisterNode(const Node:INode);
{$endIf}
{$ifDef Case4}
procedure TContainer.RegisterNode(const Node:TNode);
{$endIf}
{$ifDef Case5}
procedure TContainer.RegisterNode(Node:INode);
{$endIf}
{$ifDef Case6}
procedure TContainer.RegisterNode(const Node:INode);
{$endIf}

begin
  NodeList.Add(pointer(Node));
end;

{$ifDef Case1}
procedure TContainer.UnregisterNode(Node:INode);
{$endIf}
{$ifDef Case2}
procedure TContainer.UnregisterNode(Node:TNode);
{$endIf}
{$ifDef Case3}
procedure TContainer.UnregisterNode(const Node:INode);
{$endIf}
{$ifDef Case4}
procedure TContainer.UnregisterNode(const Node:TNode);
{$endIf}
{$ifDef Case5}
procedure TContainer.UnregisterNode(const Node:INode);
{$endIf}
{$ifDef Case6}
procedure TContainer.UnregisterNode(Node:INode);
{$endIf}
var
  i : integer;
begin
  i := NodeList.IndexOf(pointer(Node));
  if i >= 0 then
    NodeList.Delete(i);
end;

{ INode }

constructor TNode.Create(const AContainer: IContainer);
begin
  ReReg(AContainer);
end;

destructor TNode.Destroy();
begin   {When failing, after unregistering, it returns here !!!!}
  if Assigned(FContainer) then begin
    FContainer.UnregisterNode(self);
  end;
  inherited;
end;

function TNode.GetContainer(): IContainer;
begin
  Result := FContainer;
end;

procedure TNode.ReReg(const AContainer: IContainer);
begin
  if Assigned(AContainer) then
    AContainer.RegisterNode(Self);
  FContainer := AContainer;
end;

procedure TNode.SetContainer(const Value: IContainer);
begin
  if Assigned(FContainer) then
    FContainer.UnregisterNode(self);
  FContainer := Value;
  FContainer.RegisterNode(self);
end;

procedure TNode.UnReg();
begin
  if Assigned(FContainer) then
    FContainer.UnregisterNode(self);
  FContainer := nil;
end;

 { TForm1 }

procedure TForm1.btnMakeStuffClick(Sender: TObject);
begin
  MyContainer := TContainer.Create();
  MyNode1 := TNode.Create(MyContainer);
  MyNode2 := TNode.Create(MyContainer);
  MyNode3 := TNode.Create(MyContainer);

  MyNode2.UnReg();  //Breakpoint here
  MyNode2.ReReg(MyContainer);  //Breakpoint here
  MyNode3 := nil;   //Case 1 & 6 cause a stackoverflow
  MyNode2 := nil;

end;

end.

Ответы [ 3 ]

27 голосов
/ 04 октября 2011

Директива const для параметра указывает, что процедура / функция не будет изменять значение, указанное в этом параметре.Если процедура или функция желает манипулировать каким-либо параметром const , сначала необходимо скопировать это значение в локальную переменную.

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

В частности, для интерфейсов, поскольку параметр объявлен const , невозможно изменить значение передаваемой ссылки на интерфейс во время«время жизни» параметра (поскольку компилятор отклоняет любой код, который пытается изменить значение), таким образом, компилятор может исключить вызовы AddRef () и Release () в противном случае это будет сгенерировано как пролог и эпилог в этой процедуре.

Обратите внимание, однако, что в теле процедуры, если ссылка назначена другим переменным, тогда счетчик ссылок все еще может измениться.Оптимизация const просто устраняет возможную потребность в одна пара AddRef / Release.

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

На самом деле, я могу сказать вам, где вы ошиблись.:)

Вы никогда не должны напрямую приводить ссылку на интерфейс в / из любого другого типа (интерфейс или указатель или иным образом), если только вы не очень ОЧЕНЬ уверены в том, что делаете.Вы должны всегда использовать как или QueryInterface () для приведения из одного типа интерфейса к другому:

  otherRef := fooRef as IOther;

И вы всегда должны использовать IUnknown (или IInterface ) в качестве «нетипизированной» ссылки на интерфейс, а не указателя.Это гарантирует, что все ваши ссылки учитываются.( бывают случаи, когда вам нужна бесчисленная ссылка и, следовательно, используется ссылка на указатель приведения типа, но это очень продвинутый voodoo ).

В вашем примерекод, приведение к / из указателя типа для сохранения их в TList подрывает механизм подсчета ссылок и в сочетании с изменениями в const / non const параметры приводят к побочным эффектам, которые вы видите.

Чтобы поддерживать правильно подсчитанные ссылки на интерфейсы в списке, используйте класс списка, дружественный интерфейсу, такой как TList или TInterfaceList (если вам не нравятся непатентованные средства, у вас их нет или вам может понадобиться поделиться вашим кодом с кем-то, кто этого не делает).

Сноска:

Также будьте осторожны: Уничтожение объекта, когда счетчик ссылок на интерфейсы падает до нуля, не обязательно является настолько автоматическим, как вы думаете.

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

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

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

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

Я упоминаю об этом, потому что знание того, как работает вся эта очевидная «магия компилятора», может быть критически важным для пониманияпроблемы, подобные тем, с которыми вы столкнулись в этом случае.

14 голосов
/ 04 октября 2011

Подсчет ссылок для интерфейсов

Ваш первоначальный вопрос и ответ в комментариях к этому ответу полностью зависят от механизма подсчета ссылок в интерфейсе 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);

Это успешно, потому что мы явно берем ссылку.

Обратите внимание, что я объяснил это для дальнейшего использования - ваш текущий код не противоречит этой проблеме.

4 голосов
/ 04 октября 2011

Если я вас правильно понял, вы звоните UnregisterNode () из TNode.Destroy :

destructor TNode.Destroy;
begin
  ...
  UnregisterNode(Self);
  ...
end;

Вы, вероятно, делаете это, когда INode находится в конце своего срока службы, то есть когда его refcount равен 0.

Если UnregisterNode не не принимает параметр const , _ AddRef будет выполняться для Self, возвращая refcount обратно к 1, и в конце UnregisterNode будет выполнена _ Release , которая вернет refcount к 0, что означает, что Destroy вызывается снова, и ваш косвенный рекурсивный цикл вызывает переполнение стека.

Если UnregisterNode принимает параметр const , _ _ 1036 * AddRef и _ Release не выполняются, так что вы не будете попасть в рекурсивный цикл.

Такие проблемы не могут возникнуть, если вы убедитесь, что ваш RegisterNode правильно сохраняет узел, то есть увеличивает его счетчик ссылок и сохраняет его таким образом, то есть сохраняет его в безопасном списке типов, например, TList .

...