Cette question est la continuation d'un commentaire particulier de personnes sur stackoverflow que j'ai vu plusieurs fois maintenant. Avec le développeur qui m'a appris Delphi, afin de protéger les choses, j'ai toujours mis une coche if assigned()
avant de libérer des objets et avant de faire d'autres choses. Cependant, on me dit maintenant que je ne devrais pas ajouter cette vérification. J'aimerais savoir s'il y a une différence dans la façon dont l'application se compile/s'exécute si je fais cela, ou si cela n'affectera pas du tout le résultat ...
if assigned(SomeObject) then SomeObject.Free;
Disons que j'ai un formulaire, et je crée un objet bitmap en arrière-plan lors de la création du formulaire, et le libère lorsque j'en ai fini. Maintenant, je suppose que mon problème est que je me suis trop habitué à mettre cette vérification sur une grande partie de mon code lorsque j'essaie d'accéder à des objets qui pourraient potentiellement avoir été libérés à un moment donné. Je l'utilise même quand ce n'est pas nécessaire. J'aime être minutieux ...
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.
Supposons maintenant que j'introduise un nouvel objet de liste personnalisé appelé TMyList
of TMyListItem
. Pour chaque élément de cette liste, je dois bien sûr créer/libérer chaque objet élément. Il existe plusieurs façons de créer un élément, ainsi que plusieurs façons de détruire un élément (Ajouter/Supprimer étant le plus courant). Je suis sûr que c'est une très bonne pratique de mettre cette protection ici ...
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;
Dans de nombreux scénarios, j'espère au moins que l'objet est toujours créé avant d'essayer de le libérer. Mais vous ne savez jamais quels glissements pourraient se produire à l'avenir lorsqu'un objet se libère avant qu'il ne soit censé le faire. J'ai toujours utilisé ce chèque, mais maintenant on me dit que je ne devrais pas, et je ne comprends toujours pas pourquoi.
MODIFIER
Voici un exemple pour essayer de vous expliquer pourquoi j'ai l'habitude de le faire:
procedure TForm1.FormDestroy(Sender: TObject);
begin
SomeCreatedObject.Free;
if SomeCreatedObject = nil then
ShowMessage('Object is nil')
else
ShowMessage('Object is not nil');
end;
Mon point est que if SomeCreatedObject <> nil
N'est pas identique à if Assigned(SomeCreatedObject)
car après avoir libéré SomeCreatedObject
, il n'évalue pas à nil
. Les deux vérifications devraient donc être nécessaires.
Il s'agit d'une question très large sous de nombreux angles différents.
La signification de la fonction Assigned
Une grande partie du code de votre question trahit une compréhension incorrecte de la fonction Assigned
. documentation indique ceci:
Teste un pointeur nul (non assigné) ou une variable procédurale.
Utilisez Attribué pour déterminer si le pointeur ou la procédure référencée par P est nul . P doit être une référence variable d'un pointeur ou d'un type procédural.
Attribué (P) correspond au test P <> nul pour un variable pointeur, et @ P <> nil pour une variable procédurale.
Attribué renvoie Faux si P est nil , Vrai sinon.
Conseil : lorsque vous testez des événements d'objet et des procédures d'affectation, vous ne pouvez pas tester nil , et utiliser Assigned est la bonne façon.
....
Remarque : Attribué ne peut pas détecter un pointeur pendant - c'est-à-dire un ce n'est pas nul , mais cela ne pointe plus vers des données valides.
La signification de Assigned
diffère pour le pointeur et les variables procédurales. Dans le reste de cette réponse, nous ne considérerons que les variables de pointeur, car c'est le contexte de la question. Notez qu'une référence d'objet est implémentée en tant que variable de pointeur.
Les points clés à retenir de la documentation sont que, pour les variables de pointeur:
Assigned
équivaut à tester <> nil
.Assigned
ne peut pas détecter si le pointeur ou la référence d'objet est valide ou non.Dans le contexte de cette question, cela signifie que
if obj<>nil
et
if Assigned(obj)
sont complètement interchangeables.
Test de Assigned
avant d'appeler Free
L'implémentation de TObject.Free
est très spéciale.
procedure TObject.Free;
begin
if Self <> nil then
Destroy;
end;
Cela vous permet d'appeler Free
sur une référence d'objet qui est nil
et cela n'a aucun effet. Pour ce que ça vaut, je ne connais aucun autre endroit dans la RTL/VCL où une telle astuce est utilisée.
La raison pour laquelle vous souhaitez autoriser Free
à être appelée sur une référence d'objet nil
découle de la façon dont les constructeurs et les destructeurs fonctionnent dans Delphi.
Lorsqu'une exception est levée dans un constructeur, le destructeur est appelé. Cela est fait afin de désallouer toutes les ressources qui ont été allouées dans cette partie du constructeur qui a réussi. Si Free
n'était pas implémenté tel quel, alors les destructeurs devraient ressembler à ceci:
if obj1 <> nil then
obj1.Free;
if obj2 <> nil then
obj2.Free;
if obj3 <> nil then
obj3.Free;
....
La pièce suivante du puzzle est que les constructeurs Delphi initialisent la mémoire d'instance à zéro . Cela signifie que tous les champs de référence d'objet non attribués sont nil
.
Mettez tout cela ensemble et le code destructeur devient maintenant
obj1.Free;
obj2.Free;
obj3.Free;
....
Vous devez choisir cette dernière option car elle est beaucoup plus lisible.
Il existe un scénario dans lequel vous devez tester si la référence est affectée dans un destructeur. Si vous devez appeler une méthode sur l'objet avant de le détruire, vous devez clairement vous prémunir contre la possibilité qu'il soit nil
. Donc, ce code courrait le risque d'un AV s'il apparaissait dans un destructeur:
FSettings.Save;
FSettings.Free;
Au lieu de cela, vous écrivez
if Assigned(FSettings) then
begin
FSettings.Save;
FSettings.Free;
end;
Test de Assigned
en dehors d'un destructeur
Vous parlez également d'écrire du code défensif en dehors d'un destructeur. Par exemple:
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;
Dans cette situation, il n'est à nouveau pas nécessaire de tester Assigned
dans TMyObject.Update
. La raison étant que vous ne pouvez tout simplement pas appeler TMyObject.Update
À moins que le constructeur de TMyObject
ait réussi. Et si le constructeur de TMyObject
a réussi, vous savez avec certitude que FSettings
a été attribué. Encore une fois, vous rendez votre code beaucoup moins lisible et plus difficile à maintenir en faisant des appels parasites à Assigned
.
Il y a un scénario où vous devez écrire if Assigned
Et c'est là que l'existence de l'objet en question est facultative. Par exemple
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;
Dans ce scénario, la classe prend en charge deux modes de fonctionnement, avec et sans journalisation. La décision est prise au moment de la construction et toute méthode faisant référence à l'objet de journalisation doit tester son existence.
Cette forme de code non rare rend d'autant plus important que vous n'utilisez pas d'appels parasites à Assigned
pour les objets non facultatifs. Lorsque vous voyez if Assigned(FLogger)
dans le code, cela devrait vous indiquer clairement que la classe peut fonctionner normalement avec FLogger
inexistant. Si vous vaporisez des appels gratuits vers Assigned
autour de votre code, vous perdez la possibilité de dire d'un coup d'œil si un objet doit toujours exister.
Free
a une logique particulière: il vérifie si Self
est nil
, et si c'est le cas, il revient sans rien faire - vous pouvez donc appeler en toute sécurité X.Free
même si X
est nil
. Ceci est important lorsque vous écrivez des destructeurs - David a plus de détails dans sa réponse .
Vous pouvez consulter le code source de Free
pour voir comment cela fonctionne. Je n'ai pas la source Delphi à portée de main, mais c'est quelque chose comme ça:
procedure TObject.Free;
begin
if Self <> nil then
Destroy;
end;
Ou, si vous préférez, vous pouvez le considérer comme le code équivalent en utilisant Assigned
:
procedure TObject.Free;
begin
if Assigned(Self) then
Destroy;
end;
Vous pouvez écrire vos propres méthodes qui vérifient if Self <> nil
, tant qu'elles sont méthodes d'instance statiques (c'est-à-dire pas virtual
ou dynamic
) (merci à David Heffernan pour le lien de documentation). Mais dans la bibliothèque Delphi, Free
est la seule méthode que je connaisse qui utilise cette astuce.
Vous n'avez donc pas besoin de vérifier si la variable est Assigned
avant d'appeler Free
; il le fait déjà pour vous. C'est pourquoi il est recommandé d'appeler Free
plutôt que d'appeler Destroy
directement: si vous appelez Destroy sur une référence nil
, vous obtiendrez une violation d'accès.
Pourquoi vous ne devriez pas appeler
if Assigned(SomeObject) then
SomeObject.Free;
Tout simplement parce que vous exécuteriez quelque chose comme ça
if Assigned(SomeObject) then
if Assigned(SomeObject) then
SomeObject.Destroy;
Si vous appelez simplement SomeObject.Free;
alors c'est juste
if Assigned(SomeObject) then
SomeObject.Destroy;
Pour votre mise à jour, si vous avez peur de la référence d'instance d'objet, utilisez FreeAndNil. Il détruira et déférera votre objet
FreeAndNil(SomeObject);
C'est comme si vous appeliez
SomeObject.Free;
SomeObject := nil;