En Delphi, ¿por qué pasar una variable de interfaz a veces requiere que sea un parámetro const?
interface delphi-xe (3)
Conteo de referencias para interfaces
Su pregunta original y el seguimiento en los comentarios a esta respuesta dependen del mecanismo de conteo de referencias de la interfaz de Delphi.
El compilador emite código para organizar que se cuenten todas las referencias a una interfaz. Cada vez que tomas una nueva referencia, la cuenta aumenta. Cada vez que se libera una referencia (se establece en nil
, se sale del alcance, etc.) el conteo disminuye. Cuando el recuento llega a cero, la interfaz se libera y, en su caso, esto es lo que se llama Free
en sus objetos.
Su problema es que está haciendo trampa en el recuento de referencias al colocar las referencias de la interfaz dentro y fuera de la TList
de TList
mediante el TList
a Pointer
y viceversa. En algún lugar a lo largo del camino se cuentan las referencias. Estoy seguro de que el comportamiento de su código (es decir, los desbordamientos de pila) podría explicarse, pero no estoy dispuesto a intentarlo ya que el código utiliza construcciones obviamente incorrectas.
En pocas palabras, nunca debe lanzar una interfaz a un tipo no administrado como Pointer
. Siempre que lo haga, también debe tomar el control del código de conteo de referencia faltante. ¡Puedo asegurarte que esto es algo que no quieres enfrentar!
Debe usar un contenedor seguro para el tipo adecuado como TList<INode>
o incluso una matriz dinámica y luego el conteo de referencia se manejará correctamente. Hacer este cambio en su código resuelve los problemas que describe en la pregunta.
Referencias circulares
Sin embargo, aún queda un gran problema, como lo descubrió usted mismo y se detalla en los comentarios.
Una vez que sigue las reglas de conteo de referencias, se enfrenta al problema de las referencias circulares. En este caso, un nodo contiene una referencia al contenedor que a su vez contiene una referencia al nodo. Las referencias circulares como esta no se pueden romper con el mecanismo de conteo de referencias estándar y usted mismo debe romperlas. Una vez que rompe una de las dos referencias individuales que forman una referencia circular, el marco puede hacer el resto.
Con su diseño actual, debe romper las referencias circulares llamando explícitamente a UnReg
en cada INode
que cree.
El otro problema con el código tal como está es que está usando campos de datos del formulario para guardar MyContainer
, MyNode
, etc. Debido a que nunca configuró MyContainer
en nil
entonces dos ejecuciones de su controlador de eventos darán como resultado una fuga.
Hice los siguientes cambios en tu código para demostrar que se ejecutará sin fugas:
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;
Con estos cambios, el código se ejecuta sin pérdidas de memoria: establezca ReportMemoryLeaksOnShutdown := True
al inicio del archivo .dpr para verificar.
Será una especie de UnReg
tener que llamar a UnReg
en cada nodo, así que sugiero que simplemente agregue un método a IContainer
para hacerlo. Una vez que disponga que el contenedor es capaz de eliminar sus referencias, tendrá un sistema mucho más manejable.
No podrá dejar que el recuento de referencias haga todo el trabajo por usted. Deberá llamar a IContainer.UnRegAllItems
explícitamente.
Puedes implementar este nuevo método así:
procedure TContainer.UnRegAllItems;
begin
while NodeList.Count>0 do
NodeList[0].UnReg;
end;
Referencia contando errores
Aunque el mecanismo de conteo de referencias de Delphi está muy bien implementado en general, hay, por lo que sé, un error de larga data y muy conocido.
procedure Foo(const I: IInterface);
begin
I.DoSomething;
end;
...
Foo(TInterfacedObject.Create);
Cuando Foo
llama de esta manera, no se genera ningún código para agregar una referencia a la interfaz. La interfaz se libera tan pronto como se crea y Foo
actúa en una interfaz no válida.
Debido a que Foo
recibe el parámetro como const
, Foo
no toma una referencia a la interfaz. El error está en el código para la llamada a Foo
que por error no toma una referencia a la interfaz.
Mi forma preferida de solucionar este problema en particular es la siguiente:
var
I: IInterface;
...
I := TInterfacedObject.Create;
Foo(I);
Esto tiene éxito porque tomamos explícitamente una referencia.
Tenga en cuenta que he explicado esto para futuras referencias: su código actual no se enfrenta a este problema.
Primero, la pregunta: ¿Por qué la eliminación de const en UnregisterNode()
causa un error, pero no en RegisterNode()
?
Ahora los antecedentes: estoy trabajando en Delphi XE con Interfaces y me encontré con un artefacto que me ha dado un poco de pausa y he llegado a la conclusión de que no entiendo realmente por qué.
Un objeto al que se accede como una interfaz no necesita ser liberado explícitamente. Cuando la última referencia queda fuera del alcance, se destruye. Eso parece bastante simple. He escrito un caso de prueba para mostrar las variaciones que se ejecutan como se esperaba y dos que fallan. Los seis casos de prueba están limitados a variaciones en el parámetro Nodo de los métodos de registro y cancelación del registro.
Al presionar el botón solitario en el formulario se crea el contenedor y tres nodos. Se realizan operaciones sobre ellos para demostrar el procedimiento.
El programa crea algunos nodos simples que enlazan a un contenedor simple. El problema ocurrió en los casos # 1 y # 6. Cuando se libera el nodo, se llama al método Unregister()
contenedores. El método elimina una copia del puntero al nodo en una lista de distribución. Al dejar el método en los dos casos fallidos, llama al método Destroy()
del nodo y vuelve a iniciar el proceso de forma recursiva hasta que se produce un desbordamiento de pila.
En los cuatro casos que funcionan, el método Destroy()
se reanuda normalmente y el programa continuará y se cerrará normalmente.
Fallo # 1 (Caso 1)
procedure RegisterNode(Node:INode);
procedure UnregisterNode(Node:INode);
La llamada al nodo Unregister()
desde el método TNode.Destroy()
parece afectar el recuento de referencia del INode que causa múltiples llamadas a Destroy().
Por qué sucede esto, no entiendo. No sucede cuando Register()
el nodo con el mismo estilo de parámetros.
Fallo # 2 (Caso 6)
procedure RegisterNode(const Node:INode);
procedure UnregisterNode(Node:INode);
El mismo patrón de falla ocurre aquí. Agregar const a la lista de parámetros como en el Caso 5 evita las llamadas recursivas a Destroy()
.
El código:
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.
La directiva const sobre un parámetro indica que el procedimiento / función no modificará el valor suministrado en ese parámetro. Si el procedimiento o la función desea manipular cualquier parámetro const , primero deberá copiar ese valor en una variable local.
Esto le permite al compilador realizar algunas optimizaciones en dichos parámetros, particularmente en el área de tipos de referencia como cadenas e interfaces, etc.
Con las interfaces específicamente, dado que el parámetro se declara constante , es imposible modificar el valor de la referencia de interfaz que se pasó durante la "vida útil" del parámetro (ya que el compilador rechazará cualquier código que intente modificar el valor), por lo tanto el compilador puede eliminar las llamadas a AddRef () y Release () que de otra manera se generarían como prólogo y epílogo en ese procedimiento.
Sin embargo, tenga en cuenta que dentro del cuerpo del procedimiento, si la referencia se asigna a otras variables, entonces el recuento de referencias todavía podría cambiar. La optimización de const simplemente elimina la posible necesidad de un par AddRef / Release.
Esta diferencia en el comportamiento de conteo de referencias entre los parámetros const y non- const obviamente tiene algún efecto secundario u otra interacción con las otras complejidades en su código, pero ahora, al comprender el efecto de const, es posible que pueda determinar cómo / dónde pudo haber salido mal. en otra parte.
De hecho, puedo decirte dónde te has equivocado. :)
Nunca debe emitir directamente una referencia de interfaz a / desde cualquier otro tipo (interfaz o puntero u otro) a menos que esté muy MUY seguro de lo que está haciendo. Siempre debe usar como o QueryInterface () para convertir desde un tipo de interfaz a otro:
otherRef := fooRef as IOther;
Y siempre debe usar IUnknown (o IInterface ) como una referencia de interfaz ''sin tipo'', no un puntero. Esto asegura que sus referencias son todas las propiedades contabilizadas. ( Hay ocasiones en las que se desea una referencia no contabilizada y, por lo tanto, se usa una referencia de puntero de conversión de tipo, pero es un vudú muy avanzado ).
En su código de muestra, el tipo de puntero de conversión a / desde para mantenerlos en un TList está subvirtiendo el mecanismo de conteo de referencia y, junto con las variaciones en los parámetros const / non - const , está llevando a los efectos secundarios que está viendo.
Para mantener las referencias contadas correctamente a las interfaces en una lista, use una clase de lista amigable para la interfaz como TList <Tipo de interfaz> o TInterfaceList (si no le gustan los genéricos, no los tiene disponibles o puede que necesite compartir sus código con alguien que no lo hace).
Nota:
También tenga cuidado: la destrucción de un objeto cuando el recuento de referencia de la interfaz se reduce a cero no es necesariamente tan automática como cree.
Es un detalle de implementación de la clase de objeto con interfaz particular. Si inspecciona el origen de la implementación _Release () en TInterfacedObject, verá cómo esto es posible.
En pocas palabras, el objeto en sí es responsable de destruirse cuando su propio recuento de referencias llega a cero. De hecho, ¡el objeto es incluso responsable de implementar el recuento de referencia en primer lugar! Por lo tanto, es perfectamente posible (y algunas veces deseable) que una clase especializada anule o reemplace este comportamiento, en cuyo caso, la forma en que responde a un recuento de referencia cero (o incluso si le molesta mantener un recuento de referencia como tal) depende completamente de sus propias necesidades.
Dicho esto, la abrumadora mayoría de los objetos que implementan interfaces casi con seguridad utilizarán esta forma de autodestrucción, pero no se debe asumir simplemente.
Lo que debería ser seguro asumir es que si se le da una referencia de interfaz a un objeto, normalmente no estaría preocupado por la forma en que finalmente se destruirá ese objeto. Pero eso no es lo mismo que decir que puede asumir que se destruirá cuando el recuento de referencias de la interfaz llegue a cero.
Menciono esto porque ser consciente de cómo funciona toda esta aparente "magia de compilación" puede ser fundamental para comprender problemas como los que se han encontrado en este caso.
Si entendí bien, estás llamando a UnregisterNode () desde TNode.Destroy :
destructor TNode.Destroy;
begin
...
UnregisterNode(Self);
...
end;
Probablemente haga esto cuando un INode esté al final de su vida útil, es decir, cuando su refcount sea 0.
Si UnregisterNode no toma un parámetro const , se hará un _ AddRef en Self , lo que hará que el refcount vuelva a 1, y al final de UnregisterNode , se hará un _ Release , que hará que el refcount vuelva a 0, lo cual significa que se vuelve a llamar a Destroy , y existe un bucle recursivo indirecto , que provoca un desbordamiento de pila.
Si UnregisterNode toma un parámetro const , no se ejecutan _ AddRef ni ningún _ Release , por lo que no entrará en el bucle recursivo.
Dichos problemas no pueden ocurrir si se asegura de que su RegisterNode retiene correctamente el nodo, es decir, incrementa su recuento de referencia y lo mantiene así, es decir, lo almacena en una lista de tipos seguros, por ejemplo, TList <INode> .