delphi - reynosa - ¿Por qué no debería usar "si está asignado()" antes de acceder a los objetos?
delphi technologies mexico (4)
Esta es una pregunta muy amplia con muchos ángulos diferentes.
El significado de la función Assigned
Gran parte del código en su pregunta revela una comprensión incorrecta de la función Assigned
. La documentation dice esto:
Prueba un puntero nulo (no asignado) o una variable de procedimiento.
Use Asignado para determinar si el puntero o procedimiento al que hace referencia P es nulo. P debe ser una referencia variable de un puntero o tipo de procedimiento. Asignado (P) corresponde a la prueba P <> nil para una variable de puntero, y @P <> nil para una variable de procedimiento.
Retornos asignados Falso si P es nulo, Verdadero en caso contrario.
Nota : Assigned no puede detectar un puntero colgante, es decir, uno que no es nulo pero que ya no apunta a datos válidos. Por ejemplo, en el ejemplo de código para Asignado, Asignado no detecta que P no es válido.
Los puntos clave a tomar de esto son:
-
Assigned
es equivalente a probar<> nil
. -
Assigned
no puede detectar si el puntero o la referencia del objeto es válido o no.
Lo que esto significa en el contexto de esta pregunta es que
if obj<>nil
y
if Assigned(obj)
son completamente intercambiables.
Prueba Assigned
antes de llamar Free
La implementación de TObject.Free
es muy especial.
procedure TObject.Free;
begin
if Self <> nil then
Destroy;
end;
Esto le permite llamar Free
a una referencia de objeto que es nil
y hacerlo no tiene ningún efecto. Por lo que vale, no tengo conocimiento de ningún otro lugar en el RTL / VCL donde se usa ese truco.
La razón por la que desea permitir que se llame a Free
en una referencia de objeto nil
proviene de la forma en que los constructores y los destructores operan en Delphi.
Cuando se genera una excepción en un constructor, se llama al destructor. Esto se hace con el fin de desasignar los recursos asignados en la parte del constructor que tuvo éxito. Si Free
no se implementó como está, los destructores tendrían que verse así:
if obj1 <> nil then
obj1.Free;
if obj2 <> nil then
obj2.Free;
if obj3 <> nil then
obj3.Free;
....
La siguiente pieza del rompecabezas es que los constructores de Delphi inicializan la memoria de la instancia a cero . Esto significa que los campos de referencia de objetos no asignados son nil
.
Pon todo esto junto y el código destructor ahora se convierte
obj1.Free;
obj2.Free;
obj3.Free;
....
Debe elegir la última opción porque es mucho más legible.
Hay un escenario donde necesita probar si la referencia está asignada en un destructor. Si necesita llamar a cualquier método en el objeto antes de destruirlo, entonces debe evitar la posibilidad de que sea nil
. Entonces este código correría el riesgo de un AV si apareciera en un destructor:
FSettings.Save;
FSettings.Free;
En cambio, escribes
if Assigned(FSettings) then
begin
FSettings.Save;
FSettings.Free;
end;
Prueba Assigned
fuera de un destructor
También hablas de escribir código defensivo fuera de un destructor. Por ejemplo:
constructor TMyObject.Create;
begin
inherited;
FSettings := TSettings.Create;
end;
destructor TMyObject.Destroy;
begin
FSettings.Free;
inherited;
end;
procedure TMyObject.Update;
begin
if Assigned(FSettings) then
FSettings.Update;
end;
En esta situación, tampoco es necesario probar Assigned
en TMyObject.Update
. La razón es que simplemente no puede llamar a TMyObject.Update
menos que el constructor de TMyObject
. Y si el constructor de TMyObject
tuvo éxito, entonces usted está seguro de que se le asignó FSettings
. Así que de nuevo haces que tu código sea mucho menos legible y más difícil de mantener al realizar llamadas espurias a Assigned
.
Hay un escenario en el que debe escribir if Assigned
y es allí donde la existencia del objeto en cuestión es opcional. Por ejemplo
constructor TMyObject.Create(UseLogging: Boolean);
begin
inherited Create;
if UseLogging then
FLogger := TLogger.Create;
end;
destructor TMyObject.Destroy;
begin
FLogger.Free;
inherited;
end;
procedure TMyObject.FlushLog;
begin
if Assigned(FLogger) then
FLogger.Flush;
end;
En este escenario, la clase admite dos modos de operación, con y sin registro. La decisión se toma en el momento de la construcción y cualquier método que se refiera al objeto de registro debe probar su existencia.
Esta forma no común de código hace que sea aún más importante que no utilice llamadas falsas a Assigned
para objetos no opcionales. Cuando vea if Assigned(FLogger)
en el código que debería ser una clara indicación de que la clase puede funcionar normalmente con FLogger
no existe. Si rocías llamadas gratuitas a Assigned
alrededor de tu código, entonces pierdes la capacidad de decir de un vistazo si un objeto siempre debería existir o no.
Esta pregunta es una continuación de un comentario particular de personas en stackoverflow que he visto algunas veces diferentes ahora. Yo, junto con el desarrollador que me enseñó Delphi, para mantener las cosas seguras, siempre he puesto una marca if assigned()
antes de liberar objetos, y antes de hacer otras cosas. Sin embargo, ahora me dicen que no debería agregar este cheque. Me gustaría saber si existe alguna diferencia en cómo la aplicación se compila / ejecuta si hago esto, o si no afectará el resultado en absoluto ...
if assigned(SomeObject) then SomeObject.Free;
Digamos que tengo un formulario, y estoy creando un objeto de mapa de bits en el fondo sobre la creación del formulario, y liberándolo cuando termine con él. Ahora supongo que mi problema es que me acostumbré demasiado a poner esta marca en gran parte de mi código cuando intento acceder a objetos que podrían haber sido liberados en algún momento. Lo he estado usando incluso cuando no es necesario. Me gusta ser completo ...
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs;
type
TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FBitmap: TBitmap;
public
function LoadBitmap(const Filename: String): Bool;
property Bitmap: TBitmap read FBitmap;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
begin
FBitmap:= TBitmap.Create;
LoadBitmap(''C:/Some Sample Bitmap.bmp'');
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
if assigned(FBitmap) then begin //<-----
//Do some routine to close file
FBitmap.Free;
end;
end;
function TForm1.LoadBitmap(const Filename: String): Bool;
var
EM: String;
function CheckFile: Bool;
begin
Result:= False;
//Check validity of file, return True if valid bitmap, etc.
end;
begin
Result:= False;
EM:= '''';
if assigned(FBitmap) then begin //<-----
if FileExists(Filename) then begin
if CheckFile then begin
try
FBitmap.LoadFromFile(Filename);
except
on e: exception do begin
EM:= EM + ''Failure loading bitmap: '' + e.Message + #10;
end;
end;
end else begin
EM:= EM + ''Specified file is not a valid bitmap.'' + #10;
end;
end else begin
EM:= EM + ''Specified filename does not exist.'' + #10;
end;
end else begin
EM:= EM + ''Bitmap object is not assigned.'' + #10;
end;
if EM <> '''' then begin
raise Exception.Create(''Failed to load bitmap: '' + #10 + EM);
end;
end;
end.
Ahora digamos que estoy presentando un nuevo objeto de lista personalizado llamado TMyList
de TMyListItem
. Para cada elemento en esta lista, por supuesto tengo que crear / liberar cada objeto del artículo. Hay algunas formas diferentes de crear un elemento, así como algunas formas diferentes de destruir un elemento (Agregar / Eliminar es el más común). Estoy seguro de que es una muy buena práctica poner aquí esta protección ...
procedure TMyList.Delete(const Index: Integer);
var
I: TMyListItem;
begin
if (Index >= 0) and (Index < FItems.Count) then begin
I:= TMyListItem(FItems.Objects[Index]);
if assigned(I) then begin //<-----
if I <> nil then begin
I.DoSomethingBeforeFreeing(''Some Param'');
I.Free;
end;
end;
FItems.Delete(Index);
end else begin
raise Exception.Create(''My object index out of bounds (''+IntToStr(Index)+'')'');
end;
end;
En muchos escenarios, al menos espero que el objeto aún se cree antes de intentar liberarlo. Pero nunca se sabe qué deslizamientos pueden ocurrir en el futuro cuando un objeto se libera antes de lo previsto. Siempre he usado este cheque, pero ahora me dicen que no debería, y todavía no entiendo por qué.
EDITAR
Aquí hay un ejemplo para tratar de explicarte por qué tengo el hábito de hacer esto:
procedure TForm1.FormDestroy(Sender: TObject);
begin
SomeCreatedObject.Free;
if SomeCreatedObject = nil then
ShowMessage(''Object is nil'')
else
ShowMessage(''Object is not nil'');
end;
Mi punto es que if SomeCreatedObject <> nil
no es lo mismo que if Assigned(SomeCreatedObject)
porque después de liberar SomeCreatedObject
, no se evalúa como nil
. Por lo tanto, ambos controles deberían ser necesarios.
No estoy completamente seguro de eso, pero parece:
if assigned(object.owner) then object.free
funciona bien. En este ejemplo, sería
if assigned(FBitmap.owner) then FBitmap.free
Por qué no deberías llamar
if Assigned(SomeObject) then
SomeObject.Free;
Simplemente porque ejecutarías algo como esto
if Assigned(SomeObject) then
if Assigned(SomeObject) then
SomeObject.Destroy;
Si llama solo SomeObject.Free;
entonces es solo
if Assigned(SomeObject) then
SomeObject.Destroy;
Para su actualización, si tiene miedo de la referencia de la instancia del objeto, use FreeAndNil. Destruirá y desreferenciará tu objeto
FreeAndNil(SomeObject);
Es similar a si llamas
SomeObject.Free;
SomeObject := nil;
Free
tiene alguna lógica especial: comprueba si Self
es nil
, y si es así, vuelve sin hacer nada, por lo que puedes llamar a X.Free
forma X.Free
incluso si X
es nil
. Esto es importante cuando escribes destructores: David tiene más detalles en su respuesta .
Puede ver el código fuente de forma Free
para ver cómo funciona. No tengo a mano la fuente Delphi, pero es algo como esto:
procedure TObject.Free;
begin
if Self <> nil then
Destroy;
end;
O, si lo prefiere, podría considerarlo como el código equivalente usando documentation :
procedure TObject.Free;
begin
if Assigned(Self) then
Destroy;
end;
Puede escribir sus propios métodos que comprueben if Self <> nil
, siempre que sean métodos de instancia estáticos (es decir, no virtual
o dynamic
) (gracias a David Heffernan para el enlace de documentación). Pero en la biblioteca Delphi, Free
es el único método que conozco que utiliza este truco.
Por lo tanto, no necesita verificar si la variable está Assigned
antes de llamar a Free
; ya lo hace por ti. En realidad, esa es la razón por la cual la recomendación es llamar a Free
lugar de llamar directamente a Destroy
: si llamaste a Destroy en una referencia nil
, obtendrías una infracción de acceso.