delphi - ¿Debería el compilador insinuar/advertir al pasar instancias de objeto directamente como parámetros de interfaz const?
interface parameters (3)
Es un error. La conversión de la instancia a la referencia de la interfaz en RunLeakCrash debe ser una variable temporal, manteniéndola activa mientras dure RunLeakCrash.
¿El compilador debería advertir / advertir al pasar una nueva instancia de un objeto a un método que tenga un parámetro de interfaz const de una interfaz que implemente la clase del objeto?
Edición : la muestra por supuesto es simple para ilustrar el problema. Pero en la vida real se vuelve mucho más complejo: ¿y si la creación y el uso están en un código que está muy alejado (unidades diferentes, clases diferentes, proyectos diferentes)? ¿Qué pasa si es mantenido por diferentes personas? ¿Qué pasa si un parámetro no const se convierte en uno const y no se puede verificar todo el código de llamada (porque la persona que cambia el código no tiene acceso a todos los códigos de llamada)?
Codifique los siguientes bloqueos, y es muy difícil encontrar la causa.
Primero el registro:
1.Run begin
1.RunLeakCrash
2.RunLeakCrash begin
NewInstance 1
AfterConstruction 0
3.LeakCrash begin
_AddRef 1
4.Dump begin
4.Dump Reference=10394576
4.Dump end
_Release 0
_Release Destroy
BeforeDestruction 0
3.LeakCrash Reference got destroyed if it had a RefCount of 1 upon entry, so now it can be unsafe to access it
_AddRef 1
4.Dump begin
4.Dump Reference=10394576
4.Dump end
_Release 0
_Release Destroy
BeforeDestruction 0
3.LeakCrash end with exception
1.Run end
EInvalidPointer: Invalid pointer operation
Luego, el código que libera prematuramente la instancia del objeto implementando una interfaz:
//{$define all}
program InterfaceConstParmetersAndPrematureFreeingProject;
{$APPTYPE CONSOLE}
uses
SysUtils,
Windows,
MyInterfacedObjectUnit in ''../src/MyInterfacedObjectUnit.pas'';
procedure Dump(Reference: IInterface);
begin
Writeln('' 4.Dump begin'');
Writeln('' 4.Dump Reference='', Integer(PChar(Reference)));
Writeln('' 4.Dump end'');
end;
procedure LeakCrash(const Reference: IInterface);
begin
Writeln('' 3.LeakCrash begin'');
try
Dump(Reference); // now we leak because the caller does not keep a reference to us
Writeln('' 3.LeakCrash Reference got destroyed if it had a RefCount of 1 upon entry, so now it can be unsafe to access it'');
Dump(Reference); // we might crash here
except
begin
Writeln('' 3.LeakCrash end with exception'');
raise;
end;
end;
Writeln('' 3.LeakCrash end'');
end;
procedure RunLeakCrash;
begin
Writeln('' 2.RunLeakCrash begin'');
LeakCrash(TMyInterfacedObject.Create());
Writeln('' 2.RunLeakCrash end'');
end;
procedure Run();
begin
try
Writeln(''1.Run begin'');
Writeln('''');
Writeln(''1.RunLeakCrash'');
RunLeakCrash();
finally
Writeln('''');
Writeln(''1.Run end'');
end;
end;
begin
try
Run();
except
on E: Exception do
Writeln(E.ClassName, '': '', E.Message);
end;
Readln;
end.
EInvalidPointer se manifestará dentro de la segunda llamada a Dump(Reference);
. El motivo es que el recuento de referencias del objeto subyacente que expone la referencia ya es cero, por lo que el objeto subyacente ya se destruyó.
Algunas notas sobre el código de conteo de referencia insertado u omitido por el compilador:
- parámetros no marcados con
const
(como en elprocedure Dump(Reference: IInterface);
) obtener implícito los bloques try / finally para realizar el recuento de referencias. - los parámetros marcados con
const
(como en elprocedure LeakCrash(const Reference: IInterface);
) no obtienen ningún código de conteo de referencia - pasar el resultado de una creación de instancia de objeto (como
LeakCrash(TMyInterfacedObject.Create());
) no genera ningún código de recuento de referencia
Solo todos los comportamientos del compilador anteriores son muy lógicos, pero combinados pueden causar un EInvalidPointer.
EInvalidPointer se manifiesta solo en un patrón de uso muy estrecho.
El patrón es fácil de reconocer por el compilador, pero es muy difícil depurarlo o encontrar la causa cuando lo atrapó.
La solución es bastante simple: almacena en caché el resultado de TMyInterfacedObject.Create()
en una variable intermedia, y luego lo pasa a LeakCrash()
.
¿El compilador debería advertirle sobre este patrón de uso?
Finalmente, el código que utilicé para rastrear todas las llamadas a _AddRef / _Release / etccetera:
unit MyInterfacedObjectUnit;
interface
type
// Adpoted copy of TInterfacedObject for debugging
TMyInterfacedObject = class(TObject, IInterface)
protected
FRefCount: Integer;
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
public
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
class function NewInstance: TObject; override;
property RefCount: Integer read FRefCount;
end;
implementation
uses
Windows;
procedure TMyInterfacedObject.AfterConstruction;
begin
InterlockedDecrement(FRefCount);
Writeln('' AfterConstruction '', FRefCount);
end;
procedure TMyInterfacedObject.BeforeDestruction;
begin
Writeln('' BeforeDestruction '', FRefCount);
if RefCount <> 0 then
System.Error(reInvalidPtr);
end;
class function TMyInterfacedObject.NewInstance: TObject;
begin
Result := inherited NewInstance;
TMyInterfacedObject(Result).FRefCount := 1;
Writeln('' NewInstance '', TMyInterfacedObject(Result).FRefCount);
end;
function TMyInterfacedObject.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
Writeln('' QueryInterface '', FRefCount);
if GetInterface(IID, Obj) then
Result := 0
else
Result := E_NOINTERFACE;
end;
function TMyInterfacedObject._AddRef: Integer;
begin
Result := InterlockedIncrement(FRefCount);
Writeln('' _AddRef '', FRefCount);
end;
function TMyInterfacedObject._Release: Integer;
begin
Result := InterlockedDecrement(FRefCount);
Writeln('' _Release '', FRefCount);
if Result = 0 then
begin
Writeln('' _Release Destroy'');
Destroy;
end;
end;
end.
--jeroen
Yo votaría por una advertencia, ya que incluso los desarrolladores experimentados podrían caer en esta trampa. Si alguien no quiere esa advertencia, se puede desactivar fácilmente, lo que significa que no hay cambios con respecto al comportamiento actual. Es algo así como advertencias a variables no inicializadas o algo así.
Otra solución a este problema podría ser un Assert(InterfaceParameter.RefCount > 0);
implícito Assert(InterfaceParameter.RefCount > 0);
para los parámetros de interfaz const. Probablemente solo se emita cuando las aserciones están activadas.
pasar el resultado de una creación de instancia de objeto (como LeakCrash (TMyInterfacedObject.Create ());) no genera ningún código de recuento de referencia
Arriba está el error del compilador. Debe crear una var oculta y disminuir el contador cuando el procedimiento existió